From 55f2bc427f4e5aa2b3571b61c8ff7dac655768eb Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Wed, 24 May 2023 12:02:53 +0200 Subject: [PATCH] UserPassSessionAuth: ephermal session token generated token will be modulated by timestamp in nanoseconds. --- .../userpass_session_auth.zig | 62 +++++++------ src/http_auth.zig | 89 ++++++------------- 2 files changed, 61 insertions(+), 90 deletions(-) diff --git a/examples/userpass_session_auth/userpass_session_auth.zig b/examples/userpass_session_auth/userpass_session_auth.zig index 4acdc0e..5de5e05 100644 --- a/examples/userpass_session_auth/userpass_session_auth.zig +++ b/examples/userpass_session_auth/userpass_session_auth.zig @@ -2,21 +2,26 @@ const std = @import("std"); const zap = @import("zap"); const Lookup = std.StringHashMap([]const u8); -const auth_lock_token_table = false; const auth_lock_pw_table = false; // see the source for more info const Authenticator = zap.UserPassSessionAuth( Lookup, - auth_lock_pw_table, // we may set this to true if we expect our username -> password map to change - auth_lock_token_table, // we may set this to true to have session tokens deleted server-side on logout + // we may set this to true if we expect our username -> password map + // to change. in that case the authenticator must lock the table for + // every lookup + auth_lock_pw_table, ); const loginpath = "/login"; + +// we bake the login page and its displayed image into the the executable const loginpage = @embedFile("html/login.html"); const img = @embedFile("./html/Ziggy_the_Ziguana.svg.png"); // 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; // the login page (embedded) @@ -40,9 +45,13 @@ fn on_normal_page(r: zap.SimpleRequest) void { fn on_logout(r: zap.SimpleRequest) void { zap.debug("on_logout()\n", .{}); authenticator.logout(&r); + // note, the link below doesn't matter as the authenticator will send us + // straight to the /login page r.sendBody( \\ \\

You are logged out!!!

+ \\
+ \\

Log back in

\\ ) catch return; } @@ -50,12 +59,16 @@ fn on_logout(r: zap.SimpleRequest) void { fn on_request(r: zap.SimpleRequest) void { switch (authenticator.authenticateRequest(&r)) { .Handled => { - // the authenticator handled the entire request for us. probably - // a redirect to the login page + // the authenticator handled the entire request for us. + // 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", .{}); return; }, + + // never returned by this type of authenticator .AuthFailed => unreachable, + .AuthOK => { // the authenticator says it is ok to proceed as usual std.log.info("Auth OK", .{}); @@ -64,40 +77,38 @@ fn on_request(r: zap.SimpleRequest) void { // used in the login page // note: our login page is /login // 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")) { - std.log.info("Auth OK for img", .{}); r.setContentTypeFromPath() catch unreachable; r.sendBody(img) catch unreachable; return; } - // aha! got redirected to /login + // aha! probably got redirected to /login if (std.mem.startsWith(u8, p, loginpath)) { std.log.info(" + for /login --> login page", .{}); return on_login(r); } - // /logout can be shown since we're still authenticated for this - // very request + // /logout can be shown since we're authenticated if (std.mem.startsWith(u8, p, "/logout")) { std.log.info(" + for /logout --> logout page", .{}); return on_logout(r); } - // /logout can be shown since we're still authenticated for this - // very request + // /stop can be executed, as we're authenticated if (std.mem.startsWith(u8, p, "/stop")) { std.log.info(" + for /stop --> logout page", .{}); zap.stop(); 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", .{}); 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 return on_normal_page(r); }, @@ -123,33 +134,28 @@ pub fn main() !void { zap.enableDebugLog(); - // add a single user to our allowed users + // Usernames -> Passwords for the /login page + // ------------------------------------------ var userpass = Lookup.init(allocator); defer userpass.deinit(); 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( allocator, &userpass, .{ - .usernameParam = "username", - .passwordParam = "password", + .usernameParam = "username", // form param name + .passwordParam = "password", // form param name .loginPage = loginpath, - .cookieName = "zap-session", + .cookieName = "zap-session", // cookie name for session }, ); 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", .{}); // start worker threads diff --git a/src/http_auth.zig b/src/http_auth.zig index eca59aa..c62d40d 100644 --- a/src/http_auth.zig +++ b/src/http_auth.zig @@ -343,8 +343,6 @@ pub const UserPassSessionAuthArgs = struct { /// - `Lookup` must implement .get([]const u8) -> []const u8 for user password retrieval /// - `lockedPwLookups` : if true, accessing the provided Lookup instance will be protected /// 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. /// -> 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 /// -> 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. -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 { allocator: std.mem.Allocator, lookup: *Lookup, @@ -389,16 +387,6 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool .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; } @@ -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 - /// Note: only valid if lockedTokenLookups == true pub fn logout(self: *Self, r: *const zap.SimpleRequest) void { - if (lockedTokenLookups == false) { - 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 + // we erase the list of valid tokens server-side if (r.setCookie(.{ .name = self.settings.cookieName, .value = "invalid", @@ -446,7 +417,7 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool zap.debug("logout cookie setting failed: {any}\n", .{err}); } - r.parseCookies(); + r.parseCookies(false); // check for session 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(); self.tokenLookupLock.lock(); defer self.tokenLookupLock.unlock(); - // if cookie is a valid session, remove it! - _ = self.sessionTokens.remove(cookie); + if (self.sessionTokens.getKeyPtr(cookie.str)) |keyPtr| { + 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| { zap.debug("unreachable: UserPassSessionAuth.logout: {any}", .{err}); @@ -483,24 +460,14 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool if (maybe_cookie) |cookie| { defer cookie.deinit(); // locked or unlocked token lookup - if (lockedTokenLookups) { - self.tokenLookupLock.lock(); - defer self.tokenLookupLock.unlock(); - if (self.sessionTokens.contains(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}); - } + self.tokenLookupLock.lock(); + defer self.tokenLookupLock.unlock(); + if (self.sessionTokens.contains(cookie.str)) { + // cookie is a valid session! + zap.debug("Auth: COKIE IS OK!!!!: {s}\n", .{cookie.str}); + return .AuthOK; } else { - if (self.sessionTokens.contains(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}); - } + zap.debug("Auth: COKIE IS BAD!!!!: {s}\n", .{cookie.str}); } } } else |err| { @@ -588,6 +555,11 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool var hasher = Hash.init(.{}); hasher.update(username); 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; hasher.final(&digest); 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 { const token = try self.createSessionToken(username, password); - // put locked or not - if (lockedTokenLookups) { - self.tokenLookupLock.lock(); - defer self.tokenLookupLock.unlock(); + self.tokenLookupLock.lock(); + defer self.tokenLookupLock.unlock(); - if (!self.sessionTokens.contains(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), {}); - } + if (!self.sessionTokens.contains(token)) { + try self.sessionTokens.put(try self.allocator.dupe(u8, token), {}); } return token; }