1
0
Fork 0
mirror of https://github.com/zigzap/zap.git synced 2025-10-20 15:14:08 +00:00

UserPassSessionAuth: ephermal session token

generated token will be modulated by timestamp in nanoseconds.
This commit is contained in:
Rene Schallner 2023-05-24 12:02:53 +02:00
parent 9aa0ec5bef
commit 55f2bc427f
2 changed files with 61 additions and 90 deletions

View file

@ -2,21 +2,26 @@ const std = @import("std");
const zap = @import("zap"); const zap = @import("zap");
const Lookup = std.StringHashMap([]const u8); const Lookup = std.StringHashMap([]const u8);
const auth_lock_token_table = false;
const auth_lock_pw_table = false; const auth_lock_pw_table = false;
// see the source for more info // see the source for more info
const Authenticator = zap.UserPassSessionAuth( const Authenticator = zap.UserPassSessionAuth(
Lookup, Lookup,
auth_lock_pw_table, // we may set this to true if we expect our username -> password map to change // we may set this to true if we expect our username -> password map
auth_lock_token_table, // we may set this to true to have session tokens deleted server-side on logout // to change. in that case the authenticator must lock the table for
// every lookup
auth_lock_pw_table,
); );
const loginpath = "/login"; const loginpath = "/login";
// we bake the login page and its displayed image into the the executable
const loginpage = @embedFile("html/login.html"); const loginpage = @embedFile("html/login.html");
const img = @embedFile("./html/Ziggy_the_Ziguana.svg.png"); const img = @embedFile("./html/Ziggy_the_Ziguana.svg.png");
// global vars yeah! // global vars yeah!
// in bigger projects, we'd probably make use of zap.SimpleEndpoint or
// zap.Middleware and "hide" stuff like authenticators in there
var authenticator: Authenticator = undefined; var authenticator: Authenticator = undefined;
// the login page (embedded) // the login page (embedded)
@ -40,9 +45,13 @@ fn on_normal_page(r: zap.SimpleRequest) void {
fn on_logout(r: zap.SimpleRequest) void { fn on_logout(r: zap.SimpleRequest) void {
zap.debug("on_logout()\n", .{}); zap.debug("on_logout()\n", .{});
authenticator.logout(&r); authenticator.logout(&r);
// note, the link below doesn't matter as the authenticator will send us
// straight to the /login page
r.sendBody( r.sendBody(
\\ <html><body> \\ <html><body>
\\ <p>You are logged out!!!</p> \\ <p>You are logged out!!!</p>
\\ <br>
\\ <p> <a href="/">Log back in</a></p>
\\ </body></html> \\ </body></html>
) catch return; ) catch return;
} }
@ -50,12 +59,16 @@ fn on_logout(r: zap.SimpleRequest) void {
fn on_request(r: zap.SimpleRequest) void { fn on_request(r: zap.SimpleRequest) void {
switch (authenticator.authenticateRequest(&r)) { switch (authenticator.authenticateRequest(&r)) {
.Handled => { .Handled => {
// the authenticator handled the entire request for us. probably // the authenticator handled the entire request for us.
// a redirect to the login page // that means: it re-directed to the /login page because of a
// missing or invalid session cookie
std.log.info("Auth FAILED -> authenticator handled it", .{}); std.log.info("Auth FAILED -> authenticator handled it", .{});
return; return;
}, },
// never returned by this type of authenticator
.AuthFailed => unreachable, .AuthFailed => unreachable,
.AuthOK => { .AuthOK => {
// the authenticator says it is ok to proceed as usual // the authenticator says it is ok to proceed as usual
std.log.info("Auth OK", .{}); std.log.info("Auth OK", .{});
@ -64,40 +77,38 @@ fn on_request(r: zap.SimpleRequest) void {
// used in the login page // used in the login page
// note: our login page is /login // note: our login page is /login
// so, anything that starts with /login will not be touched by // so, anything that starts with /login will not be touched by
// the authenticator. Hence, we name the img /login/Ziggy....png // the authenticator. Hence, we name the img for the /login
// page: /login/Ziggy....png
if (std.mem.startsWith(u8, p, "/login/Ziggy_the_Ziguana.svg.png")) { if (std.mem.startsWith(u8, p, "/login/Ziggy_the_Ziguana.svg.png")) {
std.log.info("Auth OK for img", .{});
r.setContentTypeFromPath() catch unreachable; r.setContentTypeFromPath() catch unreachable;
r.sendBody(img) catch unreachable; r.sendBody(img) catch unreachable;
return; return;
} }
// aha! got redirected to /login // aha! probably got redirected to /login
if (std.mem.startsWith(u8, p, loginpath)) { if (std.mem.startsWith(u8, p, loginpath)) {
std.log.info(" + for /login --> login page", .{}); std.log.info(" + for /login --> login page", .{});
return on_login(r); return on_login(r);
} }
// /logout can be shown since we're still authenticated for this // /logout can be shown since we're authenticated
// very request
if (std.mem.startsWith(u8, p, "/logout")) { if (std.mem.startsWith(u8, p, "/logout")) {
std.log.info(" + for /logout --> logout page", .{}); std.log.info(" + for /logout --> logout page", .{});
return on_logout(r); return on_logout(r);
} }
// /logout can be shown since we're still authenticated for this // /stop can be executed, as we're authenticated
// very request
if (std.mem.startsWith(u8, p, "/stop")) { if (std.mem.startsWith(u8, p, "/stop")) {
std.log.info(" + for /stop --> logout page", .{}); std.log.info(" + for /stop --> logout page", .{});
zap.stop(); zap.stop();
return on_logout(r); return on_logout(r);
} }
// any other paths will still show the normal page // any other paths will show the normal page
std.log.info(" + --> normal page", .{}); std.log.info(" + --> normal page", .{});
return on_normal_page(r); return on_normal_page(r);
} }
// if there is no path, we're still authenticated, so let's show // if there is no path but we're authenticated, so let's show
// the user something // the user something
return on_normal_page(r); return on_normal_page(r);
}, },
@ -123,33 +134,28 @@ pub fn main() !void {
zap.enableDebugLog(); zap.enableDebugLog();
// add a single user to our allowed users // Usernames -> Passwords for the /login page
// ------------------------------------------
var userpass = Lookup.init(allocator); var userpass = Lookup.init(allocator);
defer userpass.deinit(); defer userpass.deinit();
try userpass.put("zap", "awesome"); try userpass.put("zap", "awesome");
// init our auth // init our authenticator. it will redirect all un-authenticated
// requests to the /login page. on POST of correct username, password
// pair, it will create an ephermal session token and let subsequent
// requests that present the cookie through until, in our case: /logout.
authenticator = try Authenticator.init( authenticator = try Authenticator.init(
allocator, allocator,
&userpass, &userpass,
.{ .{
.usernameParam = "username", .usernameParam = "username", // form param name
.passwordParam = "password", .passwordParam = "password", // form param name
.loginPage = loginpath, .loginPage = loginpath,
.cookieName = "zap-session", .cookieName = "zap-session", // cookie name for session
}, },
); );
defer authenticator.deinit(); defer authenticator.deinit();
// just some debug output: listing the session tokens the authenticator may
// have generated already (if auth_lock_token_table == false).
const lookup = authenticator.sessionTokens;
std.debug.print("\nauth token list len: {d}\n", .{lookup.count()});
var it = lookup.iterator();
while (it.next()) |item| {
std.debug.print(" {s}\n", .{item.key_ptr.*});
}
std.debug.print("Visit me on http://127.0.0.1:3000\n", .{}); std.debug.print("Visit me on http://127.0.0.1:3000\n", .{});
// start worker threads // start worker threads

View file

@ -343,8 +343,6 @@ pub const UserPassSessionAuthArgs = struct {
/// - `Lookup` must implement .get([]const u8) -> []const u8 for user password retrieval /// - `Lookup` must implement .get([]const u8) -> []const u8 for user password retrieval
/// - `lockedPwLookups` : if true, accessing the provided Lookup instance will be protected /// - `lockedPwLookups` : if true, accessing the provided Lookup instance will be protected
/// by a Mutex. You can access the mutex yourself via the `passwordLookupLock`. /// by a Mutex. You can access the mutex yourself via the `passwordLookupLock`.
/// - `lockedTokenLookups` : if true, accessing the internal token table will be protected
/// by a Mutex. You can access the mutex yourself via the `passwordLookupLock`.
/// ///
/// Note: In order to be quick, you can set lockedTokenLookups to false. /// Note: In order to be quick, you can set lockedTokenLookups to false.
/// -> we generate it on init() and leave it static /// -> we generate it on init() and leave it static
@ -353,7 +351,7 @@ pub const UserPassSessionAuthArgs = struct {
/// -> another browser program with the page still open would still be able to use /// -> another browser program with the page still open would still be able to use
/// -> the session. Which is kindof OK, but not as cool as erasing the token /// -> the session. Which is kindof OK, but not as cool as erasing the token
/// -> on the server side which immediately block all other browsers as well. /// -> on the server side which immediately block all other browsers as well.
pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool, comptime lockedTokenLookups: bool) type { pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool) type {
return struct { return struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
lookup: *Lookup, lookup: *Lookup,
@ -389,16 +387,6 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
.sessionTokens = SessionTokenMap.init(allocator), .sessionTokens = SessionTokenMap.init(allocator),
}; };
if (lockedTokenLookups == false) {
// we populate on init and forbid logout()
var it = lookup.iterator();
while (it.next()) |kv| {
// we iterate over all usernames and passwords, create tokens,
// and memorize the tokens
const token = try ret.createAndStoreSessionToken(kv.key_ptr.*, kv.value_ptr.*);
allocator.free(token);
}
}
return ret; return ret;
} }
@ -417,25 +405,8 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
} }
/// Check for session token cookie, remove the token from the valid tokens /// Check for session token cookie, remove the token from the valid tokens
/// Note: only valid if lockedTokenLookups == true
pub fn logout(self: *Self, r: *const zap.SimpleRequest) void { pub fn logout(self: *Self, r: *const zap.SimpleRequest) void {
if (lockedTokenLookups == false) { // we erase the list of valid tokens server-side
if (r.setCookie(.{
.name = self.settings.cookieName,
.value = "invalid",
.max_age_s = -1,
})) {
zap.debug("logout ok\n", .{});
} else |err| {
zap.debug("logout cookie setting failed: {any}\n", .{err});
}
// @compileLog("WARNING! If lockedTokenLookups==false, logout() cannot erase the token from its internal, server-side list of valid tokens");
return;
} else {
zap.debug("logout cookie setting failed\n", .{});
}
// if we are allowed to lock the table, we can erase the list of valid tokens server-side
if (r.setCookie(.{ if (r.setCookie(.{
.name = self.settings.cookieName, .name = self.settings.cookieName,
.value = "invalid", .value = "invalid",
@ -446,7 +417,7 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
zap.debug("logout cookie setting failed: {any}\n", .{err}); zap.debug("logout cookie setting failed: {any}\n", .{err});
} }
r.parseCookies(); r.parseCookies(false);
// check for session cookie // check for session cookie
if (r.getCookieStr(self.settings.cookieName, self.allocator, false)) |maybe_cookie| { if (r.getCookieStr(self.settings.cookieName, self.allocator, false)) |maybe_cookie| {
@ -454,8 +425,14 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
defer cookie.deinit(); defer cookie.deinit();
self.tokenLookupLock.lock(); self.tokenLookupLock.lock();
defer self.tokenLookupLock.unlock(); defer self.tokenLookupLock.unlock();
// if cookie is a valid session, remove it! if (self.sessionTokens.getKeyPtr(cookie.str)) |keyPtr| {
_ = self.sessionTokens.remove(cookie); const keySlice = keyPtr.*;
// if cookie is a valid session, remove it!
_ = self.sessionTokens.remove(cookie.str);
// only now can we let go of the cookie str slice that
// was used as the key
self.allocator.free(keySlice);
}
} }
} else |err| { } else |err| {
zap.debug("unreachable: UserPassSessionAuth.logout: {any}", .{err}); zap.debug("unreachable: UserPassSessionAuth.logout: {any}", .{err});
@ -483,24 +460,14 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
if (maybe_cookie) |cookie| { if (maybe_cookie) |cookie| {
defer cookie.deinit(); defer cookie.deinit();
// locked or unlocked token lookup // locked or unlocked token lookup
if (lockedTokenLookups) { self.tokenLookupLock.lock();
self.tokenLookupLock.lock(); defer self.tokenLookupLock.unlock();
defer self.tokenLookupLock.unlock(); if (self.sessionTokens.contains(cookie.str)) {
if (self.sessionTokens.contains(cookie.str)) { // cookie is a valid session!
// cookie is a valid session! zap.debug("Auth: COKIE IS OK!!!!: {s}\n", .{cookie.str});
zap.debug("Auth: COKIE IS OK!!!!: {s}\n", .{cookie.str}); return .AuthOK;
return .AuthOK;
} else {
zap.debug("Auth: COKIE IS BAD!!!!: {s}\n", .{cookie.str});
}
} else { } else {
if (self.sessionTokens.contains(cookie.str)) { zap.debug("Auth: COKIE IS BAD!!!!: {s}\n", .{cookie.str});
// cookie is a valid session!
zap.debug("Auth: COKIE IS OK!!!!: {s}\n", .{cookie.str});
return .AuthOK;
} else {
zap.debug("Auth: COKIE IS BAD!!!!: {s}\n", .{cookie.str});
}
} }
} }
} else |err| { } else |err| {
@ -588,6 +555,11 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
var hasher = Hash.init(.{}); var hasher = Hash.init(.{});
hasher.update(username); hasher.update(username);
hasher.update(password); hasher.update(password);
var buf: [16]u8 = undefined;
const time_nano = std.time.nanoTimestamp();
const timestampHex = try std.fmt.bufPrint(&buf, "{0x}", .{time_nano});
hasher.update(timestampHex);
var digest: [Hash.digest_length]u8 = undefined; var digest: [Hash.digest_length]u8 = undefined;
hasher.final(&digest); hasher.final(&digest);
const token: Token = std.fmt.bytesToHex(digest, .lower); const token: Token = std.fmt.bytesToHex(digest, .lower);
@ -597,18 +569,11 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
fn createAndStoreSessionToken(self: *Self, username: []const u8, password: []const u8) ![]const u8 { fn createAndStoreSessionToken(self: *Self, username: []const u8, password: []const u8) ![]const u8 {
const token = try self.createSessionToken(username, password); const token = try self.createSessionToken(username, password);
// put locked or not self.tokenLookupLock.lock();
if (lockedTokenLookups) { defer self.tokenLookupLock.unlock();
self.tokenLookupLock.lock();
defer self.tokenLookupLock.unlock();
if (!self.sessionTokens.contains(token)) { if (!self.sessionTokens.contains(token)) {
try self.sessionTokens.put(try self.allocator.dupe(u8, token), {}); try self.sessionTokens.put(try self.allocator.dupe(u8, token), {});
}
} else {
if (!self.sessionTokens.contains(token)) {
try self.sessionTokens.put(try self.allocator.dupe(u8, token), {});
}
} }
return token; return token;
} }