srctree

Gregory Mullen parent 22296a6f 7a5c7081
create cookie sessions

src/auth.zig added: 266, removed: 9, total 257
@@ -8,12 +8,15 @@ pub const Error = error{
NotProvided,
Unauthenticated,
UnknownUser,
NoSpaceLeft,
OutOfMemory,
};
 
/// TODO document
pub const MTLS = struct {
base: ?Provider = null,
 
/// TODO document misuse of default without a base provider
pub fn authenticate(ptr: *anyopaque, headers: *const Headers) Error!User {
const mtls: *MTLS = @ptrCast(@alignCast(ptr));
var success: bool = false;
@@ -61,6 +64,7 @@ pub const MTLS = struct {
.valid = valid,
.lookup_user = lookupUser,
.create_session = null,
.get_cookie = null,
},
};
}
@@ -93,6 +97,217 @@ test MTLS {
// TODO there's likely a few more error states we should validate;
}
 
/// Default CookieAuth Helper uses Sha256 as the HMAC primitive.
pub const CookieAuth = cookieAuth(hmac.sha2.HmacSha256);
 
pub const cookie_auth = struct {
/// Why are you using sha1?
pub const sha1 = cookieAuth(hmac.HmacSha1);
 
pub const sha2 = struct {
pub const @"224" = cookieAuth(hmac.sha2.HmacSha224);
pub const @"256" = cookieAuth(hmac.sha2.HmacSha256);
pub const @"384" = cookieAuth(hmac.sha2.HmacSha384);
pub const @"512" = cookieAuth(hmac.sha2.HmacSha512);
};
};
 
/// TODO document
pub fn cookieAuth(Hmac: type) type {
return struct {
hmac: ?Hmac,
base: ?Provider,
// we don't currently store the key, but we may need to because it's not
// stored within hmac after init
//server_secret_key: []const u8,
/// Max age in seconds a session cookie is valid for.
max_age: usize,
cookie_name: []const u8 = "verse_session_secret",
 
/// this session buffer API is unstable and may be replaced
session_buffer: [ibuf_size]u8 = [_]u8{0} ** ibuf_size,
alloc: ?Allocator,
const ibuf_size = b64_enc.calcSize(Hmac.mac_length * 8);
 
pub const Self = @This();
 
pub fn init(opts: struct {
alloc: ?Allocator = null,
base: ?Provider = null,
server_secret_key: []const u8,
max_age: usize = 86400 * 365,
}) Self {
return .{
.hmac = Hmac.init(opts.server_secret_key),
.base = opts.base,
.max_age = opts.max_age,
.alloc = opts.alloc,
};
}
 
pub fn authenticate(ptr: *anyopaque, headers: *const Headers) Error!User {
const ca: *Self = @ptrCast(@alignCast(ptr));
if (ca.base) |base| {
if (headers.get("Cookie")) |cookies| {
if (cookies.value_list.next != null) return error.InvalidAuth;
const cookie = cookies.value_list.value;
std.debug.print("cookie: {s} \n", .{cookie});
if (std.mem.indexOf(u8, cookie, ca.cookie_name)) |i| {
return base.lookupUser(cookie[i..]);
}
}
}
return .{ .user_ptr = null };
}
 
pub fn valid(ptr: *anyopaque, user: *const User) bool {
const ca: *Self = @ptrCast(@alignCast(ptr));
if (ca.base) |base| return base.valid(user);
return false;
}
 
pub fn lookupUser(ptr: *anyopaque, user_id: []const u8) Error!User {
const ca: *Self = @ptrCast(@alignCast(ptr));
if (ca.base) |base| return base.lookupUser(user_id);
return error.UnknownUser;
}
 
pub fn mkToken(hm: *Hmac, token: []u8, user: *const User) Error!usize {
const time = toBytes(nativeToLittle(i64, std.time.timestamp()));
 
var buffer: [Self.ibuf_size]u8 = [_]u8{0} ** Self.ibuf_size;
var b: []u8 = buffer[0..];
hm.update(time[0..8]);
@memcpy(b[0..8], time[0..8]);
b = b[8..];
 
if (user.username) |un| {
hm.update(un);
@memcpy(b[0..un.len], un);
b[un.len] = ':';
b = b[un.len + 1 ..];
}
if (user.session_extra_data) |ed| {
hm.update(ed);
@memcpy(b[0..ed.len], ed);
b[ed.len] = ':';
b = b[ed.len + 1 ..];
}
hm.final(b[0..Hmac.mac_length]);
b = b[Hmac.mac_length..];
 
const final = buffer[0 .. buffer.len - b.len];
if (token.len < b64_enc.calcSize(final.len)) return error.NoSpaceLeft;
return b64_enc.encode(token, final).len;
}
 
pub fn createSession(ptr: *anyopaque, user: *User) Error!void {
const ca: *Self = @ptrCast(@alignCast(ptr));
if (ca.base) |base| try base.createSession(user);
 
const prefix_len: usize = (if (user.username) |u| u.len + 1 else 0) +
if (user.session_extra_data) |ed| ed.len + 1 else 0;
if (prefix_len > ca.session_buffer.len / 2) {
if (ca.alloc == null) return error.NoSpaceLeft;
return error.NoSpaceLeft;
}
 
const len = try mkToken(&ca.hmac.?, ca.session_buffer[0..], user);
user.session_next = ca.session_buffer[0..len];
}
 
pub fn getCookie(ptr: *anyopaque, user: User) Error!?Cookie {
const ca: *Self = @ptrCast(@alignCast(ptr));
if (ca.base) |base| return base.getCookie(user);
 
if (user.session_next) |next| {
return .{
.name = "verse_session_secret",
.value = next,
.attr = .{
// TODO only set when HTTPS is enabled
.secure = true,
.same_site = .strict,
},
};
}
return null;
}
 
pub fn provider(ca: *Self) Provider {
return .{
.ctx = ca,
.vtable = .{
.authenticate = authenticate,
.valid = valid,
.lookup_user = lookupUser,
.create_session = createSession,
.get_cookie = getCookie,
},
};
}
};
}
 
test CookieAuth {
const a = std.testing.allocator;
{
var auth = CookieAuth.init(.{
.alloc = a,
.server_secret_key = "This may surprise you; but this secret_key is more secure than most of the secret keys in prod use",
});
const provider = auth.provider();
 
var user = User{
.username = "testing user",
};
 
try provider.createSession(&user);
 
try std.testing.expect(user.session_next != null);
const cookie = try provider.getCookie(user);
 
try std.testing.expect(cookie != null);
try std.testing.expectStringStartsWith(cookie.?.value, user.session_next.?);
try std.testing.expectEqual(12 + 18 + 42, cookie.?.value.len);
try std.testing.expectStringStartsWith(cookie.?.value[8..], "AAB0ZXN0aW5nIHVzZXI6");
var dec_buf: [88]u8 = undefined;
const len = try b64_dec.calcSizeForSlice(cookie.?.value);
try b64_dec.decode(dec_buf[0..len], cookie.?.value);
const decoded = dec_buf[0..len];
try std.testing.expectStringStartsWith(decoded[8..], "testing user:");
}
 
{
var auth = CookieAuth.init(.{
.alloc = a,
.server_secret_key = "This may surprise you; but this secret_key is more secure than most of the secret keys in prod use",
});
const provider = auth.provider();
 
var user = User{
.username = "testing user",
.session_extra_data = "extra data",
};
 
try provider.createSession(&user);
 
try std.testing.expect(user.session_next != null);
const cookie = try provider.getCookie(user);
 
try std.testing.expect(cookie != null);
try std.testing.expectStringStartsWith(cookie.?.value, user.session_next.?);
try std.testing.expectEqual(12 + 18 + 16 + 42, cookie.?.value.len);
try std.testing.expectStringStartsWith(cookie.?.value[8..], "AAB0ZXN0aW5nIHVzZXI6ZXh0cmEgZGF0YT");
var dec_buf: [88]u8 = undefined;
const len = try b64_dec.calcSizeForSlice(cookie.?.value);
try b64_dec.decode(dec_buf[0..len], cookie.?.value);
const decoded = dec_buf[0..len];
try std.testing.expectStringStartsWith(decoded[8..], "testing user:");
try std.testing.expectStringStartsWith(decoded[21..], "extra data:");
}
}
 
pub const InvalidAuth = struct {
pub fn provider() Provider {
return Provider{
@@ -102,6 +317,7 @@ pub const InvalidAuth = struct {
.valid = valid,
.lookup_user = lookupUser,
.create_session = null,
.get_cookie = null,
},
};
}
@@ -144,6 +360,7 @@ const TestingAuth = struct {
.valid = null,
.lookup_user = lookupUserUntyped,
.create_session = null,
.get_cookie = null,
},
};
}
@@ -168,4 +385,10 @@ test Provider {
// should be explicit which is being used.
const std = @import("std");
const Allocator = std.mem.Allocator;
const toBytes = std.mem.toBytes;
const nativeToLittle = std.mem.nativeToLittle;
const hmac = std.crypto.auth.hmac;
const b64_enc = std.base64.url_safe.Encoder;
const b64_dec = std.base64.url_safe.Decoder;
const Cookie = @import("cookies.zig").Cookie;
const Headers = @import("headers.zig");
 
src/auth/provider.zig added: 266, removed: 9, total 257
@@ -10,17 +10,20 @@ pub const VTable = struct {
lookup_user: ?LookupUserFn,
valid: ?ValidFn,
create_session: ?CreateSessionFn,
get_cookie: ?GetCookieFn,
 
pub const AuthenticateFn = *const fn (*anyopaque, *const Headers) Error!User;
pub const LookupUserFn = *const fn (*anyopaque, []const u8) Error!User;
pub const ValidFn = *const fn (*anyopaque, *const User) bool;
pub const CreateSessionFn = *const fn (*anyopaque, *const User) Error!void;
pub const CreateSessionFn = *const fn (*anyopaque, *User) Error!void;
pub const GetCookieFn = *const fn (*anyopaque, User) Error!?Cookie;
 
pub const Empty = .{
.authenticate = null,
.lookup_user = null,
.valid = null,
.create_session = null,
.get_cookie = null,
};
};
 
@@ -57,6 +60,16 @@ pub fn createSession(self: *const Provider, user: *User) Error!void {
return error.NotProvided;
}
 
/// Note getCookie will return `null` instead of an error when no function is
/// provided.
pub fn getCookie(self: *const Provider, user: User) Error!?Cookie {
if (self.vtable.get_cookie) |func| {
return try func(self.ctx, user);
}
 
return null;
}
 
test "Provider" {
const std = @import("std");
const p = Provider{
@@ -68,9 +81,11 @@ test "Provider" {
try std.testing.expectEqual(false, p.valid(undefined));
try std.testing.expectError(error.NotProvided, p.lookupUser(undefined));
try std.testing.expectError(error.NotProvided, p.createSession(undefined));
try std.testing.expectEqual(null, p.getCookie(undefined));
}
 
const Auth = @import("../auth.zig");
pub const Error = Auth.Error;
const Headers = @import("../headers.zig");
const Cookie = @import("../cookies.zig").Cookie;
const User = @import("user.zig");
 
src/auth/user.zig added: 266, removed: 9, total 257
@@ -1,9 +1,28 @@
//! This is a default User provided by Verse. This is almost certainly not what
//! you want.
user_ptr: ?*anyopaque,
//! Auth.User is a thick user wrapper used within Auth.Provider and the other
//! default authentication providers within Verse. Many of the fields here are
//! provided here for convenience and aren't (currently) used within the
//! Providers.
 
/// deprecated do not use
username: []const u8 = undefined,
/// Reserved for callers. Never modified by any Verse Provider.
user_ptr: ?*anyopaque = null,
/// Reserved for callers.
username: ?[]const u8 = null,
 
// The following fields are used and modified by Verse Providers.
 
/// The **currently* active session for the user. The session that was used to
/// create the object. Often or likely included with the current user request.
/// May be the same as session_next.
/// See also: next_session
session_current: ?[]const u8 = null,
/// Newly created, and expected to be the session used with the next request
/// (when possible).
/// See also: session_current
session_next: ?[]const u8 = null,
 
/// session_extra_data is embedded within the session token which is returned in
/// clear text back to client
session_extra_data: ?[]const u8 = null,
 
const User = @This();