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

UserPassSession Authentication!!!

This commit is contained in:
Rene Schallner 2023-05-09 04:35:59 +02:00
parent 645db5c8d0
commit 340b1ae3a8
9 changed files with 560 additions and 1 deletions

View file

@ -47,6 +47,10 @@ Here's what works:
- **[cookies](examples/cookies/cookies.zig)**: a simple example sending - **[cookies](examples/cookies/cookies.zig)**: a simple example sending
itself a cookie and responding with a session cookie. itself a cookie and responding with a session cookie.
- **[websockets](examples/websockets/)**: a simple websockets chat for the browser. - **[websockets](examples/websockets/)**: a simple websockets chat for the browser.
- **[Username/Password Session Authentication](./examples/userpass_session_auth/)**:
A convenience authenticator that redirects un-authenticated requests to a
login page and sends cookies containing session tokens based on
username/password pairs transmitted via POST request.
I'll continue wrapping more of facil.io's functionality and adding stuff to zap I'll continue wrapping more of facil.io's functionality and adding stuff to zap

View file

@ -53,6 +53,7 @@ pub fn build(b: *std.build.Builder) !void {
.{ .name = "http_params", .src = "examples/http_params/http_params.zig" }, .{ .name = "http_params", .src = "examples/http_params/http_params.zig" },
.{ .name = "cookies", .src = "examples/cookies/cookies.zig" }, .{ .name = "cookies", .src = "examples/cookies/cookies.zig" },
.{ .name = "websockets", .src = "examples/websockets/websockets.zig" }, .{ .name = "websockets", .src = "examples/websockets/websockets.zig" },
.{ .name = "userpass_session", .src = "examples/userpass_session_auth/userpass_session_auth.zig" },
}) |excfg| { }) |excfg| {
const ex_name = excfg.name; const ex_name = excfg.name;
const ex_src = excfg.src; const ex_src = excfg.src;

View file

@ -1,6 +1,6 @@
.{ .{
.name = "zap", .name = "zap",
.version = "0.0.16", .version = "0.0.17",
.dependencies = .{ .dependencies = .{
.@"facil.io" = .{ .@"facil.io" = .{

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

View file

@ -0,0 +1,90 @@
<html>
<head>
<style>
/* Bordered form */
form {
border: 3px solid #f1f1f1;
}
/* Full-width inputs */
input[type=text], input[type=password] {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
}
/* Set a style for all buttons */
button {
background-color: #04AA6D;
color: white;
padding: 14px 20px;
margin: 8px 0;
border: none;
cursor: pointer;
width: 100%;
}
/* Add a hover effect for buttons */
button:hover {
opacity: 0.8;
}
/* Extra style for the cancel button (red) */
.cancelbtn {
width: auto;
padding: 10px 18px;
background-color: #f44336;
}
/* Center the avatar image inside this container */
.imgcontainer {
text-align: center;
margin: 24px 0 12px 0;
}
/* Avatar image */
img.avatar {
width: 40%;
border-radius: 50%;
}
/* Add padding to containers */
.container {
padding: 16px;
}
/* Change styles for span and cancel button on extra small screens */
@media screen and (max-width: 300px) {
span.psw {
display: block;
float: none;
}
.cancelbtn {
width: 100%;
}
}
</style>
</head>
<body>
<form action="normal_page" method="post"> <!-- we post directly to the page we want to display if login is successful-->
<div class="imgcontainer">
<img src="/login/Ziggy_the_Ziguana.svg.png" alt="Avatar" class="avatar">
</div>
<div class="container">
<label for="username"><b>Username</b></label>
<input type="text" placeholder="Enter Username" name="username" required>
<label for="password"><b>Password</b></label>
<input type="password" placeholder="Enter Password" name="password" required>
<button type="submit">Login</button>
</div>
</form>
</body>
</html>

View file

@ -0,0 +1,147 @@
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
);
const loginpath = "/login";
const loginpage = @embedFile("html/login.html");
const img = @embedFile("./html/Ziggy_the_Ziguana.svg.png");
// global vars yeah!
var authenticator: Authenticator = undefined;
// the login page (embedded)
fn on_login(r: zap.SimpleRequest) void {
r.sendBody(loginpage) catch return;
}
// the "normal page"
fn on_normal_page(r: zap.SimpleRequest) void {
zap.debug("on_normal_page()\n", .{});
r.sendBody(
\\ <html><body>
\\ <h1>Hello from ZAP!!!</h1>
\\ <p>You are logged in!!!</>
\\ <center><a href="/logout">logout</a></center>
\\ </body></html>
) catch return;
}
// the logged-out page
fn on_logout(r: zap.SimpleRequest) void {
zap.debug("on_logout()\n", .{});
authenticator.logout(&r);
r.sendBody(
\\ <html><body>
\\ <p>You are logged out!!!</p>
\\ </body></html>
) catch return;
}
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
std.log.info("Authenticator handled it", .{});
return;
},
.AuthFailed => unreachable,
.AuthOK => {
// the authenticator says it is ok to proceed as usual
std.log.info("Auth OK", .{});
// dispatch to target path
if (r.path) |p| {
// 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
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
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
if (std.mem.startsWith(u8, p, "/logout")) {
std.log.info(" + for /logout --> logout page", .{});
return on_logout(r);
}
// any other paths will still 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
// the user something
return on_normal_page(r);
},
}
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{
.thread_safe = true,
}){};
var allocator = gpa.allocator();
var listener = zap.SimpleHttpListener.init(.{
.port = 3000,
.on_request = on_request,
.log = true,
.max_clients = 100000,
});
try listener.listen();
zap.enableDebugLog();
// add a single user to our allowed users
var userpass = Lookup.init(allocator);
try userpass.put("zap", "awesome");
// init our auth
authenticator = try Authenticator.init(
allocator,
&userpass,
.{
.usernameParam = "username",
.passwordParam = "password",
.loginPage = loginpath,
.cookieName = "zap-session",
},
);
// 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
zap.start(.{
.threads = 2,
.workers = 2,
});
}

View file

@ -300,3 +300,308 @@ pub fn BearerAuthMulti(comptime Lookup: type) type {
} }
}; };
} }
pub const UserPassSessionAuthArgs = struct {
/// username body parameter
usernameParam: []const u8,
/// password body parameter
passwordParam: []const u8,
/// redirect to this page if auth fails
loginPage: []const u8,
/// name of the cookie
cookieName: []const u8,
/// cookie max age in seconds; 0 -> session cookie
cookieMaxAge: u8 = 0,
/// redirect status code, defaults to 302 found
redirectCode: zap.StatusCode = .found,
};
/// UserPassSessionAuth supports the following use case:
///
/// - checks every request: is it going to the login page? -> let the request through.
/// - else:
/// - checks every request for a session token in a cookie
/// - if there is no token, it checks for correct username and password body params
/// - if username and password are present and correct, it will create a session token,
/// create a response cookie containing the token, and carry on with the request
/// - else it will redirect to the login page
/// - if the session token is present and correct: it will let the request through
/// - else: it will redirect to the login page
///
/// Please note the implications of this simple approach: IF YOU REUSE "username"
/// and "password" body params for anything else in your application, then the
/// mechanisms described above will kick in. For that reason: please know what you're
/// doing.
///
/// See UserPassSessionAuthArgs:
/// - username & password param names can be defined by you
/// - session cookie name and max-age can be defined by you
/// - login page and redirect code (.302) can be defined by you
///
/// Comptime Parameters:
///
/// - `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
/// -> there is no way to 100% log out apart from re-starting the server
/// -> because: we send a cookie to the browser that invalidates the session cookie
/// -> 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 {
return struct {
allocator: std.mem.Allocator,
lookup: *Lookup,
settings: UserPassSessionAuthArgs,
// TODO: cookie store per user
sessionTokens: SessionTokenMap,
passwordLookupLock: std.Thread.Mutex = .{},
tokenLookupLock: std.Thread.Mutex = .{},
const Self = @This();
const SessionTokenMap = std.StringHashMap(void);
const Hash = std.crypto.hash.sha2.Sha256;
const Token = [Hash.digest_length * 2]u8;
pub fn init(
allocator: std.mem.Allocator,
lookup: *Lookup,
args: UserPassSessionAuthArgs,
) !Self {
var ret: Self = .{
.allocator = allocator,
.settings = .{
.usernameParam = try allocator.dupe(u8, args.usernameParam),
.passwordParam = try allocator.dupe(u8, args.passwordParam),
.loginPage = try allocator.dupe(u8, args.loginPage),
.cookieName = try allocator.dupe(u8, args.cookieName),
.cookieMaxAge = args.cookieMaxAge,
.redirectCode = args.redirectCode,
},
.lookup = lookup,
.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
_ = try ret.createAndStoreSessionToken(kv.key_ptr.*, kv.value_ptr.*);
}
}
return ret;
}
/// 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 = self.settings.cookieMaxAge,
})) {
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(.{
.name = self.settings.cookieName,
.value = "invalid",
.max_age_s = self.settings.cookieMaxAge,
})) {
zap.debug("logout ok\n", .{});
} else |err| {
zap.debug("logout cookie setting failed: {any}\n", .{err});
}
r.parseCookies();
// check for session cookie
if (r.getCookieStr(self.settings.cookieName, self.allocator, false)) |maybe_cookie| {
if (maybe_cookie) |cookie| {
defer cookie.deinit();
self.tokenLookupLock.lock();
defer self.tokenLookupLock.unlock();
// if cookie is a valid session, remove it!
_ = self.sessionTokens.remove(cookie);
}
} else |err| {
zap.debug("unreachable: UserPassSessionAuth.logout: {any}", .{err});
}
}
pub fn deinit(self: *const Self) void {
self.allocator.free(self.settings.usernameParam);
self.allocator.free(self.settings.passwordParam);
self.allocator.free(self.settings.loginPage);
self.allocator.free(self.settings.cookieName);
}
fn _internal_authenticateRequest(self: *Self, r: *const zap.SimpleRequest) AuthResult {
// if we're requesting the login page, let the request through
if (r.path) |p| {
if (std.mem.startsWith(u8, p, self.settings.loginPage)) {
return .AuthOK;
}
}
// parse body
r.parseBody() catch {
// zap.debug("warning: parseBody() failed in UserPassSessionAuth: {any}", .{err});
// this is not an error in case of e.g. gets with querystrings
};
r.parseCookies(false);
// check for session cookie
if (r.getCookieStr(self.settings.cookieName, self.allocator, false)) |maybe_cookie| {
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});
}
} 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});
}
}
}
} else |err| {
zap.debug("unreachable: could not check for cookie in UserPassSessionAuth: {any}", .{err});
}
// get params of username and password
if (r.getParamStr(self.settings.usernameParam, self.allocator, false)) |maybe_username| {
if (maybe_username) |*username| {
defer username.deinit();
if (r.getParamStr(self.settings.passwordParam, self.allocator, false)) |maybe_pw| {
if (maybe_pw) |*pw| {
defer pw.deinit();
// now check
const correct_pw_optional = brk: {
if (lockedPwLookups) {
self.passwordLookupLock.lock();
defer self.passwordLookupLock.unlock();
break :brk self.lookup.*.get(username.str);
} else {
break :brk self.lookup.*.get(username.str);
}
};
if (correct_pw_optional) |correct_pw| {
if (std.mem.eql(u8, pw.str, correct_pw)) {
// create session token
if (self.createAndStoreSessionToken(username.str, pw.str)) |token| {
// now set the cookie header
if (r.setCookie(.{
.name = self.settings.cookieName,
.value = token,
.max_age_s = self.settings.cookieMaxAge,
})) {
return .AuthOK;
} else |err| {
zap.debug("could not set session token: {any}", .{err});
}
} else |err| {
zap.debug("could not create session token: {any}", .{err});
}
// errors with token don't mean the auth itself wasn't OK
return .AuthOK;
}
}
}
} else |err| {
zap.debug("getParamSt() for password failed in UserPassSessionAuth: {any}", .{err});
return .AuthFailed;
}
}
} else |err| {
zap.debug("getParamSt() for user failed in UserPassSessionAuth: {any}", .{err});
return .AuthFailed;
}
return .AuthFailed;
}
pub fn authenticateRequest(self: *Self, r: *const zap.SimpleRequest) AuthResult {
switch (self._internal_authenticateRequest(r)) {
.AuthOK => {
// username and pass are ok -> created token, set header, caller can continue
return .AuthOK;
},
// this does not happen, just for completeness
.Handled => return .Handled,
// auth failed -> redirect
.AuthFailed => {
// we need to redirect and return .Handled
self.redirect(r) catch |err| {
// we just give up
zap.debug("redirect() failed in UserPassSessionAuth: {any}", .{err});
};
return .Handled;
},
}
}
fn redirect(self: *Self, r: *const zap.SimpleRequest) !void {
try r.redirectTo(self.settings.loginPage, self.settings.redirectCode);
}
fn createSessionToken(self: *Self, username: []const u8, password: []const u8) ![]const u8 {
var hasher = Hash.init(.{});
hasher.update(username);
hasher.update(password);
var digest: [Hash.digest_length]u8 = undefined;
hasher.final(&digest);
const token: Token = std.fmt.bytesToHex(digest, .lower);
const token_str = try self.allocator.dupe(u8, token[0..token.len]);
return token_str;
}
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();
if (!self.sessionTokens.contains(token)) {
try self.sessionTokens.put(token, {});
}
} else {
if (!self.sessionTokens.contains(token)) {
try self.sessionTokens.put(token, {});
}
}
return token;
}
};
}

View file

@ -97,6 +97,13 @@ pub const SimpleRequest = struct {
return self.setHeader("content-type", s); return self.setHeader("content-type", s);
} }
// redirect to path with status code 302 by default
pub fn redirectTo(self: *const Self, path: []const u8, code: ?_module.StatusCode) HttpError!void {
self.setStatus(if (code) |status| status else .found);
try self.setHeader("Location", path);
try self.sendBody("moved");
}
/// shows how to use the logger /// shows how to use the logger
pub fn setContentTypeWithLogger( pub fn setContentTypeWithLogger(
self: *const Self, self: *const Self,

View file

@ -7,4 +7,9 @@ endpoint
wrk wrk
mustache mustache
endpoint_auth endpoint_auth
http_params
pkghash pkghash
cookies
websockets
userpass_session