Skip to content

Instantly share code, notes, and snippets.

@jebjerg
Last active March 24, 2023 15:11
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jebjerg/d1c4a23057d5f35a8157 to your computer and use it in GitHub Desktop.
Save jebjerg/d1c4a23057d5f35a8157 to your computer and use it in GitHub Desktop.
nginx 2fa authentication layer (lua + Go)
// Swap values for CHANGE FOR YOURSELF, and OBS: it's a novelty authentication, so improvements can and will happen
package main
import (
"bufio"
"crypto/hmac"
"crypto/sha1"
"fmt"
"github.com/craigmj/gototp"
"github.com/jeramey/go-pwhash/sha512_crypt"
"log"
"net/http"
"os"
"strings"
"time"
)
// TODO: ?next=..., sha256 cookie
const DOMAIN = ".localhost" // CHANGE FOR YOURSELF
const SECURE_COOKIE = true
const TOTP_SECRET_PATH = "/var/auth/2fa/%v/.google_authenticator"
const SHADOWFILE = "/var/auth/shadow" // CHANGE FOR YOURSELF
func TOTP_Secret(user string) (string, error) {
if len(user) > 0 {
auth_file, err := os.Open(fmt.Sprintf(TOTP_SECRET_PATH, user))
if err != nil {
return "", err
}
defer auth_file.Close()
scanner := bufio.NewScanner(auth_file)
scanner.Scan()
secret := scanner.Text()
if len(secret) >= 16 {
return secret, nil
}
}
return "", fmt.Errorf("bad user '%v'", user)
}
func CheckPassword(username, password string) bool {
shadow, err := os.Open(SHADOWFILE)
if err != nil {
fmt.Println("err:", err)
return false
}
defer shadow.Close()
scanner := bufio.NewScanner(shadow)
for scanner.Scan() {
shadow_parts := strings.SplitN(scanner.Text(), ":", 3)
shadow_user, shadow_hash := shadow_parts[0], shadow_parts[1]
if shadow_user == username {
crypt_parts := strings.SplitN(shadow_hash, "$", 3)
id := crypt_parts[1]
if id != "6" {
fmt.Println("WARN! id not 6, refusing")
return false
}
return sha512_crypt.Verify(password, shadow_hash)
}
}
return false
}
func Authenticate(w http.ResponseWriter, req *http.Request) {
req.ParseForm()
username, password, code := req.Form.Get("username"),
req.Form.Get("password"),
req.Form.Get("code")
secret, err := TOTP_Secret(username)
if err != nil {
http.Redirect(w, req, /* CHANGE FOR YOURSELF */, http.StatusTemporaryRedirect)
return
}
otp, err := gototp.New(secret)
if err != nil {
http.Redirect(w, req, /* CHANGE FOR YOURSELF */, http.StatusTemporaryRedirect)
return
}
if CheckPassword(username, password) &&
(code == fmt.Sprintf("%06d", otp.FromNow(-1)) ||
code == fmt.Sprintf("%06d", otp.Now()) ||
code == fmt.Sprintf("%06d", otp.FromNow(1))) {
SignResponse(w, username)
http.Redirect(w, req, "/", http.StatusFound)
return
}
http.Redirect(w, req, /* CHANGE FOR YOURSELF */, http.StatusTemporaryRedirect)
return
}
const CookieMaxAge = 4 * time.Hour
func SignResponse(w http.ResponseWriter, username string) {
expiration := /*username +*/ fmt.Sprintf("%v", int(time.Now().Unix())+3600)
mac := hmac.New(sha1.New, []byte(NAME_OF_COOKIE and SIGNING_SECRET_CHOOSE_FOR_YOURSELF))
mac.Write([]byte(expiration))
hash := fmt.Sprintf("%x", mac.Sum(nil))
value := fmt.Sprintf("%v:%v", expiration, hash)
cookieContent := fmt.Sprintf("%v=%v", NAME_OF_COOKIE, value)
expire := time.Now().Add(CookieMaxAge)
cookie := http.Cookie{NAME_OF_COOKIE,
value,
"/",
DOMAIN,
expire,
expire.Format(time.UnixDate),
int(CookieMaxAge.Seconds()),
SECURE_COOKIE,
true,
cookieContent,
[]string{cookieContent},
}
http.SetCookie(w, &cookie)
}
func main() {
port := ":8080"
if p := os.Getenv("PORT"); len(p) > 0 {
port = fmt.Sprintf(":%s", p)
}
http.HandleFunc("/authenticate/verify", Authenticate)
http.Handle("/", http.StripPrefix("/authenticate/", http.FileServer(http.Dir("./static"))))
fmt.Println("2FA HTTP layer listening")
if err := http.ListenAndServe(port, nil); err != nil {
log.Fatal("Unable to create HTTP layer", err)
}
}
-- set macros: NAME_OF_COOKIE and SIGNING_SECRET_CHOOSE_FOR_YOURSELF
local cookie = ngx.var.cookie_NAME_OF_COOKIE
local hmac = ""
local timestamp = ""
-- check le cookie
if cookie ~= nil and cookie:find(":") ~= nil then
-- split cookie into HMAC signature and timestamp.
local divider = cookie:find(":")
hmac = cookie:sub(divider+1)
timestamp = cookie:sub(0, divider-1)
local secret = SIGNING_SECRET_CHOOSE_FOR_YOURSELF
-- Verify that the signature is valid.
if ndk.set_var.set_encode_hex(ngx.hmac_sha1(secret, timestamp)) == hmac and tonumber(timestamp) >= os.time() then
return
end
end
--
-- redirect no valid cookie found
ngx.redirect("/authenticate#next="..ngx.var.uri)
<html>
<head>
<title>login</title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<form method="POST" action="/authenticate/verify" class="form-signin">
<label for="username" class="sr-only">Email address</label>
<input type="input" name="username" id="username" class="form-control" placeholder="Username" required autofocus>
<label for="password" class="sr-only">Password</label>
<input type="password" name="password" id="password" class="form-control" placeholder="Password" required autofocus>
<label for="code" class="sr-only">Verification code</label>
<input type="password" name="code" id="code" class="form-control" placeholder="Verification code" required>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</div>
</body>
<script>
document.querySelector("form").action += '?' + location.hash.substr(1);
</script>
</html>
// bla bla omitted
http {
upstream app_server {
server 127.0.0.1:5000;
}
server {
listen 443;
server_name localhost;
access_log /var/log/nginx/access443.log main;
root /var/www;
ssl on;
// ...
location / {
root /var/www;
access_by_lua_file /var/auth/auth.lua;
}
location /app {
access_by_lua_file /var/auth/auth.lua;
proxy_set_header x-real-ip $remote_addr;
proxy_set_header x-forwarded-for $proxy_add_x_forwarded_for;
proxy_set_header host $http_host;
proxy_set_header x-forwarded-proto https;
proxy_redirect off;
client_max_body_size 4m;
client_body_buffer_size 128k;
proxy_pass http://app_server/;
}
location /authenticate {
proxy_pass http://localhost:8080;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment