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:
parent
9aa0ec5bef
commit
55f2bc427f
2 changed files with 61 additions and 90 deletions
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue