I've finally got Basic HTTP authentication working in a manner that is relatively secure, user-friendly, and has a logout.
This solution uses jQuery to send credentials supplied in an HTML for as a Basic Authorization header to NGINX using ajax. It doesn't look like HTTP auth from the user perspective - it doesn't even revert back to it if the auth fails.
It is secure because there are actually two ajax requests; one to get a random salt from the server (updating the db in the process), and another to send the authorization header. The first request is to prevent replay attacks. Before the second ajax request, the salt and the password are hashed so that the password is never sent in the clear.
Nginx is capable of overriding the status of an error, in this case, the 401 status code gets changed to 403, not authorized, to avoid having the browser prompt the user for their credentials again.
location = /cgi-bin/authenticate.cgi {
auth_pam "Login";
default_type text/plain;
error_page 401 =403 @hmm;
echo '';
}
location @hmm {
echo '';
}
location /cgi-bin/salt.cgi {
access_log off;
content_by_lua_file '/usr/local/lib/salt.lua';
}
The /usr/local/lib/salt.lua file:
luasql = require "luasql.postgres"
json = require "json"
mysalt = math.random(1000,10000).."h"
envv = assert (luasql.postgres())
con = assert (envv:connect("dbname", "dbuser", "dbpass", "127.0.0.1", 5432))
res = assert(con:execute("UPDATE account SET salt = '"..mysalt.."' WHERE email = '"..ngx.req.get_uri_args()["email"].."' OR username = '"..ngx.req.get_uri_args()["email"].."'"))
con:close()
envv:close()
ngx.header.content_type = "application/json"
ngx.say(json.encode( {salt=mysalt} ))
// courtest of stack overflow...
function make_base_auth(user, password) {
var tok = user + ':' + password;
var hash = btoa(tok);
return "Basic " + hash;
}
$('#userLogin').submit(function(e) {
e.preventDefault();
var username = $('#inputEmail').val();
$.ajax({
url : '/cgi-bin/salt.cgi',
type : 'GET',
async : false,
data : { email: username },
dataType : 'json'
}).done(function(json) {
var salt = json["salt"];
var hash = hex_sha256($('#inputPassword').val());
var password = hex_hmac_sha256(salt, hash);
$.ajax({
url : '/cgi-bin/authenticate.cgi',
type : 'GET',
headers : { Authorization: make_base_auth(username, password) },
success : function(){
setTimeout(function() { window.location = '/'; }, 2000);
},
error : function(){ console.log('error'); }
});
});
});
$(".logout").live('click', function(e) {
e.preventDefault();
$.ajax({
url : '/cgi-bin/authenticate.cgi',
type : 'GET',
username : 'anony',
password : 'mouse'
});
});
Part of the /etc/pam_pgsql.conf file:
pw_type=clear
auth_query = select encode(hmac(password, salt, 'sha256'),'hex') as password \
from account where (username = %u OR email = %u) and email_confirmed IS NOT FALSE
The password is stored in the database as a sha256 hash, but I set it as clear in the conf file so that I can do the hmac in the auth query.
Security is best when scrutinized. I'd welcome your feedback, questions, or concerns.
I'm not using this solution anymore, but it was fun to explore - doing so taught me a lot about security. I am using something similar now:
I probably will buy an SSL certificate at some point, but I'd rather not. I really don't see them as being much more secure. There are many websites where the only truly private data that gets transferred or not stored in plaintext on the server is the password.