Last active
March 24, 2023 15:11
-
-
Save jebjerg/d1c4a23057d5f35a8157 to your computer and use it in GitHub Desktop.
nginx 2fa authentication layer (lua + Go)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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