diff --git a/doc/authentication.md b/doc/authentication.md
new file mode 100644
index 0000000..2472b24
--- /dev/null
+++ b/doc/authentication.md
@@ -0,0 +1,253 @@
+# Authentication
+
+Zap supports both Basic and Bearer authentication.
+
+For convenience, Authenticator types exist that can authenticate requests.
+
+Zap also provides an `AuthenticatingEndpoint` endpoint-wrapper.
+
+Have a look at the tests: [here](../src/test_auth.zig)
+
+The following describes the Authenticator types. All of them provide the
+`authenticateRequest()` function, which takes a `zap.SimpleRequest` and returns
+a bool value whether it could be authenticated or not.
+
+Further down, we show how to use the Authenticators, and also the
+`AuthenticatingEndpoint`.
+
+## Basic Authentication
+
+The `zap.BasicAuth` Authenticator accepts 2 comptime values:
+
+- `Lookup`: either a map to look up passwords for users or a set to lookup
+ base64 encoded tokens (user:pass -> base64-encode = token)
+- `kind` :
+ - `UserPass` : decode the authentication header, split into user and
+ password, then lookup the password in the provided map and compare it.
+ - `Token68` : don't bother decoding, the 'lookup' set is filled with
+ base64-encoded tokens, so a fast lookup is enough.
+
+Maps passed in as `Lookup` type must support `get([]const u8)`, and sets must
+support `contains([]const u8)`.
+
+## Bearer Authentication
+
+The `zap.BearerAuthSingle` Authenticator is a convenience-authenticator that
+takes a single auth token. If all you need is to protect your prototype with a
+token, this is the one you want to use.
+
+`zap.BearerAuthMulti` accepts a map (`Lookup`) that needs to support
+`contains([]const u8)`.
+
+## Request Authentication
+
+Here we describe how to authenticate requests from within your `on_request`
+callback.
+
+### Single-Token Bearer Authentication
+
+```zig
+const std = @import("std");
+const zap = @import("zap");
+
+const allocator = std.heap.page_allocator;
+const token = "hello, world";
+
+var auth = try zap.BearerAuthSingle.init(allocator, token, null);
+defer auth.deinit();
+
+
+fn on_request(r: zap.SimpleRequest) void {
+ if(authenticator.authenticateRequest(r)) {
+ r.sendBody(
+ \\
+ \\ Hello from ZAP!!!
+ \\
+ ) catch return;
+ } else {
+ r.setStatus(.unauthorized);
+ r.sendBody("UNAUTHORIZED") catch return;
+ }
+}
+```
+
+### Multi-Token Bearer Authentication
+
+```zig
+const std = @import("std");
+const zap = @import("zap");
+
+const allocator = std.heap.page_allocator;
+const token = "hello, world";
+
+const Set = std.StringHashMap(void);
+var set = Set.init(allocator); // set
+defer set.deinit();
+
+// insert auth tokens
+try set.put(token, {});
+
+var auth = try zap.BearerAuthMulti(Set).init(allocator, &set, null);
+defer auth.deinit();
+
+
+fn on_request(r: zap.SimpleRequest) void {
+ if(authenticator.authenticateRequest(r)) {
+ r.sendBody(
+ \\
+ \\ Hello from ZAP!!!
+ \\
+ ) catch return;
+ } else {
+ r.setStatus(.unauthorized);
+ r.sendBody("UNAUTHORIZED") catch return;
+ }
+}
+```
+
+### UserPass Basic Authentication
+
+```zig
+const std = @import("std");
+const zap = @import("zap");
+
+const allocator = std.heap.page_allocator;
+
+// create a set of User -> Pass entries
+const Map = std.StringHashMap([]const u8);
+var map = Map.init(allocator);
+defer map.deinit();
+
+// create user / pass entry
+const user = "Alladdin";
+const pass = "opensesame";
+try map.put(user, pass);
+
+// create authenticator
+const Authenticator = zap.BasicAuth(Map, .UserPass);
+var auth = try Authenticator.init(a, &map, null);
+defer auth.deinit();
+
+
+fn on_request(r: zap.SimpleRequest) void {
+ if(authenticator.authenticateRequest(r)) {
+ r.sendBody(
+ \\
+ \\ Hello from ZAP!!!
+ \\
+ ) catch return;
+ } else {
+ r.setStatus(.unauthorized);
+ r.sendBody("UNAUTHORIZED") catch return;
+ }
+}
+```
+
+
+### Token68 Basic Authentication
+
+```zig
+const std = @import("std");
+const zap = @import("zap");
+
+const allocator = std.heap.page_allocator;
+const token = "QWxhZGRpbjpvcGVuIHNlc2FtZQ==";
+
+// create a set of Token68 entries
+const Set = std.StringHashMap(void);
+var set = Set.init(allocator); // set
+defer set.deinit();
+try set.put(token, {});
+
+// create authenticator
+const Authenticator = zap.BasicAuth(Set, .Token68);
+var auth = try Authenticator.init(allocator, &set, null);
+defer auth.deinit();
+
+
+fn on_request(r: zap.SimpleRequest) void {
+ if(authenticator.authenticateRequest(r)) {
+ r.sendBody(
+ \\
+ \\ Hello from ZAP!!!
+ \\
+ ) catch return;
+ } else {
+ r.setStatus(.unauthorized);
+ r.sendBody("UNAUTHORIZED") catch return;
+ }
+}
+```
+
+## AuthenticatingEndpoint
+
+Here, we only show using one of the Authenticator types. See the tests for more
+examples.
+
+The `AuthenticatingEndpoint` honors `.unauthorized` in the endpoint settings, where you can pass in a callback to deal with unauthorized requests. If you leave it to `null`, the endpoint will automatically reply with a `401 - Unauthorized` response.
+
+The example below should make clear how to wrap an endpoint into an
+`AuthenticatingEndpoint`:
+
+```zig
+const std = @import("std");
+const zap = @import("zap");
+
+const a = std.heap.page_allocator;
+const token = "ABCDEFG";
+
+// authenticated requests go here
+fn endpoint_http_get(e: *zap.SimpleEndpoint, r: zap.SimpleRequest) void {
+ _ = e;
+ r.sendBody(HTTP_RESPONSE) catch return;
+ received_response = HTTP_RESPONSE;
+ zap.fio_stop();
+}
+
+// just for fun, we also catch the unauthorized callback
+fn endpoint_http_unauthorized(e: *zap.SimpleEndpoint, r: zap.SimpleRequest) void {
+ _ = e;
+ r.setStatus(.unauthorized);
+ r.sendBody("UNAUTHORIZED ACCESS") catch return;
+ std.debug.print("\nunauthorized\n", .{});
+ received_response = "UNAUTHORIZED";
+ zap.fio_stop();
+}
+
+
+// setup listener
+var listener = zap.SimpleEndpointListener.init(
+ a,
+ .{
+ .port = 3000,
+ .on_request = null,
+ .log = false,
+ .max_clients = 10,
+ .max_body_size = 1 * 1024,
+ },
+);
+defer listener.deinit();
+
+// create mini endpoint
+var ep = zap.SimpleEndpoint.init(.{
+ .path = "/test",
+ .get = endpoint_http_get,
+ .unauthorized = endpoint_http_unauthorized,
+});
+
+// create authenticator
+const Authenticator = zap.BearerAuthSingle;
+var authenticator = try Authenticator.init(a, token, null);
+defer authenticator.deinit();
+
+// create authenticating endpoint
+const BearerAuthEndpoint = zap.AuthenticatingEndpoint(Authenticator);
+var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
+
+try listener.addEndpoint(auth_ep.getEndpoint());
+
+listener.listen() catch {};
+std.debug.print("Listening on 0.0.0.0:3000\n", .{});
+```
+
+
diff --git a/src/http_auth.zig b/src/http_auth.zig
index 6a384de..7590329 100644
--- a/src/http_auth.zig
+++ b/src/http_auth.zig
@@ -204,10 +204,10 @@ pub const BearerAuthSingle = struct {
/// Errors:
/// HTTP/1.1 401 Unauthorized
/// WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="..."
-pub fn BearerAuthMulti(comptime T: type) type {
+pub fn BearerAuthMulti(comptime Lookup: type) type {
return struct {
allocator: std.mem.Allocator,
- lookup: *T,
+ lookup: *Lookup,
realm: ?[]const u8,
const Self = @This();
@@ -215,7 +215,7 @@ pub fn BearerAuthMulti(comptime T: type) type {
/// Creates a BasicAuth. `lookup` must implement `.get([]const u8) -> []const u8`
/// to look up tokens
/// if realm is provided (not null), a copy is taken -> call deinit() to clean up
- pub fn init(allocator: std.mem.Allocator, lookup: *T, realm: ?[]const u8) !Self {
+ pub fn init(allocator: std.mem.Allocator, lookup: *Lookup, realm: ?[]const u8) !Self {
return .{
.allocator = allocator,
.lookup = lookup,
diff --git a/src/http_client_testrunner.zig b/src/http_client_testrunner.zig
index 20b91ab..b2790d3 100644
--- a/src/http_client_testrunner.zig
+++ b/src/http_client_testrunner.zig
@@ -2,6 +2,8 @@ const std = @import("std");
pub fn main() !void {
const a = std.heap.page_allocator;
+
+ // Bearer Single
var p = std.ChildProcess.init(&.{
"./zig-out/bin/http_client",
"http://127.0.0.1:3000/test",
@@ -22,6 +24,28 @@ pub fn main() !void {
std.time.sleep(3 * std.time.ns_per_s);
+ // Bearer Multi
+ p = std.ChildProcess.init(&.{
+ "./zig-out/bin/http_client",
+ "http://127.0.0.1:3000/test",
+ "Bearer",
+ "ABCDEFG",
+ }, a);
+ _ = try p.spawnAndWait();
+
+ std.time.sleep(3 * std.time.ns_per_s);
+
+ p = std.ChildProcess.init(&.{
+ "./zig-out/bin/http_client",
+ "http://127.0.0.1:3000/test",
+ "Bearer",
+ "invalid",
+ }, a);
+ _ = try p.spawnAndWait();
+
+ std.time.sleep(3 * std.time.ns_per_s);
+
+ // Basic
p = std.ChildProcess.init(&.{
"./zig-out/bin/http_client",
"http://127.0.0.1:3000/test",
diff --git a/src/test_auth.zig b/src/test_auth.zig
index 0e97fac..6c2091b 100644
--- a/src/test_auth.zig
+++ b/src/test_auth.zig
@@ -23,12 +23,13 @@ test "BearerAuthMulti authenticate" {
const a = std.testing.allocator;
const token = "hello, world";
- var map = std.StringHashMap(void).init(a); // set
- defer map.deinit();
+ const Set = std.StringHashMap(void);
+ var set = Set.init(a); // set
+ defer set.deinit();
- try map.put(token, {});
+ try set.put(token, {});
- var auth = try Authenticators.BearerAuthMulti(@TypeOf(map)).init(a, &map, null);
+ var auth = try Authenticators.BearerAuthMulti(Set).init(a, &set, null);
defer auth.deinit();
// invalid auth header
@@ -65,7 +66,7 @@ test "BasicAuth UserPass" {
// create a set of User -> Pass entries
const Map = std.StringHashMap([]const u8);
- var map = Map.init(a); // set
+ var map = Map.init(a);
defer map.deinit();
// create user / pass entry
@@ -287,6 +288,61 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" {
.unauthorized = endpoint_http_unauthorized,
});
+ const Set = std.StringHashMap(void);
+ var set = Set.init(a); // set
+ defer set.deinit();
+
+ // insert auth tokens
+ try set.put(token, {});
+
+ const Authenticator = Authenticators.BearerAuthMulti(Set);
+ var authenticator = try Authenticator.init(a, &set, null);
+ defer authenticator.deinit();
+
+ // create authenticating endpoint
+ const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator);
+ var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
+
+ try listener.addEndpoint(auth_ep.getEndpoint());
+
+ listener.listen() catch {};
+ std.debug.print("Listening on 0.0.0.0:3000\n", .{});
+ std.debug.print("Waiting for the following:\n", .{});
+ std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Bearer invalid", .{});
+
+ // start worker threads
+ zap.start(.{
+ .threads = 1,
+ .workers = 0,
+ });
+
+ try std.testing.expectEqualStrings("UNAUTHORIZED", received_response);
+}
+
+test "BearerAuthMulti authenticateRequest OK" {
+ const a = std.testing.allocator;
+ const token = "ABCDEFG";
+
+ // setup listener
+ var listener = zap.SimpleEndpointListener.init(
+ a,
+ .{
+ .port = 3000,
+ .on_request = null,
+ .log = false,
+ .max_clients = 10,
+ .max_body_size = 1 * 1024,
+ },
+ );
+ defer listener.deinit();
+
+ // create mini endpoint
+ var ep = Endpoints.SimpleEndpoint.init(.{
+ .path = "/test",
+ .get = endpoint_http_get,
+ .unauthorized = endpoint_http_unauthorized,
+ });
+
// create authenticator
const Authenticator = Authenticators.BearerAuthSingle;
var authenticator = try Authenticator.init(a, token, null);
@@ -300,8 +356,8 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" {
listener.listen() catch {};
std.debug.print("Listening on 0.0.0.0:3000\n", .{});
- std.debug.print("Please run the following:\n", .{});
- std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Bearer invalid", .{});
+ std.debug.print("Waiting for the following:\n", .{});
+ std.debug.print("./zig-out/bin/http_client_runner http://127.0.0.1:3000/test Bearer invalid\n", .{});
// start worker threads
zap.start(.{
@@ -309,7 +365,56 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" {
.workers = 0,
});
- try std.testing.expectEqualStrings("UNAUTHORIZED", received_response);
+ try std.testing.expectEqualStrings(HTTP_RESPONSE, received_response);
+}
+
+test "BearerAuthMulti authenticateRequest test-unauthorized" {
+ const a = std.testing.allocator;
+ const token = "invalid";
+
+ // setup listener
+ var listener = zap.SimpleEndpointListener.init(
+ a,
+ .{
+ .port = 3000,
+ .on_request = null,
+ .log = false,
+ .max_clients = 10,
+ .max_body_size = 1 * 1024,
+ },
+ );
+ defer listener.deinit();
+
+ // create mini endpoint
+ var ep = Endpoints.SimpleEndpoint.init(.{
+ .path = "/test",
+ .get = endpoint_http_get,
+ .unauthorized = endpoint_http_unauthorized,
+ });
+
+ // create authenticator
+ const Authenticator = Authenticators.BearerAuthSingle;
+ var authenticator = try Authenticator.init(a, token, null);
+ defer authenticator.deinit();
+
+ // create authenticating endpoint
+ const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator);
+ var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
+
+ try listener.addEndpoint(auth_ep.getEndpoint());
+
+ listener.listen() catch {};
+ std.debug.print("Listening on 0.0.0.0:3000\n", .{});
+ std.debug.print("Waiting for the following:\n", .{});
+ std.debug.print("./zig-out/bin/http_client_runner http://127.0.0.1:3000/test Bearer invalid\n", .{});
+
+ // start worker threads
+ zap.start(.{
+ .threads = 1,
+ .workers = 0,
+ });
+
+ try std.testing.expectEqualStrings(HTTP_RESPONSE, received_response);
}
test "BasicAuth Token68 authenticateRequest" {
@@ -354,7 +459,7 @@ test "BasicAuth Token68 authenticateRequest" {
listener.listen() catch {};
std.debug.print("Listening on 0.0.0.0:3000\n", .{});
- std.debug.print("Please run the following:\n", .{});
+ std.debug.print("Waiting for the following:\n", .{});
std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Basic " ++ token, .{});
// start worker threads
@@ -408,7 +513,7 @@ test "BasicAuth Token68 authenticateRequest test-unauthorized" {
listener.listen() catch {};
std.debug.print("Listening on 0.0.0.0:3000\n", .{});
- std.debug.print("Please run the following:\n", .{});
+ std.debug.print("Waiting for the following:\n", .{});
std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Basic " ++ "invalid", .{});
// start worker threads
@@ -445,7 +550,7 @@ test "BasicAuth UserPass authenticateRequest" {
// create a set of User -> Pass entries
const Map = std.StringHashMap([]const u8);
- var map = Map.init(a); // set
+ var map = Map.init(a);
defer map.deinit();
// create user / pass entry
@@ -473,7 +578,7 @@ test "BasicAuth UserPass authenticateRequest" {
listener.listen() catch {};
std.debug.print("Listening on 0.0.0.0:3000\n", .{});
- std.debug.print("Please run the following:\n", .{});
+ std.debug.print("Waiting for the following:\n", .{});
std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Basic {s}\n", .{encoded});
// start worker threads
@@ -510,7 +615,7 @@ test "BasicAuth UserPass authenticateRequest test-unauthorized" {
// create a set of User -> Pass entries
const Map = std.StringHashMap([]const u8);
- var map = Map.init(a); // set
+ var map = Map.init(a);
defer map.deinit();
// create user / pass entry
@@ -538,7 +643,7 @@ test "BasicAuth UserPass authenticateRequest test-unauthorized" {
listener.listen() catch {};
std.debug.print("Listening on 0.0.0.0:3000\n", .{});
- std.debug.print("Please run the following:\n", .{});
+ std.debug.print("Waiting for the following:\n", .{});
std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Basic {s}-invalid\n", .{encoded});
// start worker threads
diff --git a/src/zap.zig b/src/zap.zig
index 01a46a0..9284b1b 100644
--- a/src/zap.zig
+++ b/src/zap.zig
@@ -9,6 +9,7 @@ pub usingnamespace @import("endpoint.zig");
pub usingnamespace @import("util.zig");
pub usingnamespace @import("http.zig");
pub usingnamespace @import("mustache.zig");
+pub usingnamespace @import("http_auth.zig");
pub const Log = @import("log.zig");