const std = @import("std"); const auth = @import("../auth/tokens.zig"); const clock = @import("../core/clock.zig"); const config = @import("../core/config.zig"); const http_api = @import("../http/api.zig"); const store = @import("../storage/store.zig"); const cookie_name = "__Host-zds-browser"; pub const lifetime: i64 = 365 * 24 * 60 * 60; pub fn token(request: *const http_api.Request) ?[]const u8 { var cookies = std.mem.splitScalar(u8, http_api.headerValue(request, "cookie") orelse return null, ';'); while (cookies.next()) |part| { const cookie = std.mem.trim(u8, part, " \t"); if (std.mem.startsWith(u8, cookie, cookie_name ++ "=")) { const value = cookie[cookie_name.len + 1 ..]; if (value.len != 64) return null; for (value) |c| if (!std.ascii.isHex(c)) return null; return value; } } return null; } fn digest(value: []const u8) [64]u8 { var hash: [32]u8 = undefined; std.crypto.hash.sha2.Sha256.hash(value, &hash, .{}); return std.fmt.bytesToHex(hash, .lower); } pub fn account(request: *const http_api.Request, allocator: std.mem.Allocator) !?auth.Account { const value = token(request) orelse return null; return store.getBrowserSession(allocator, &digest(value), clock.nowSeconds()); } pub fn sameOrigin(request: *const http_api.Request) bool { const origin = http_api.headerValue(request, "origin") orelse return false; return std.mem.eql(u8, origin, config.publicUrl()); } pub fn csrf(value: []const u8, request_id: []const u8) [64]u8 { var mac: [32]u8 = undefined; std.crypto.auth.hmac.sha2.HmacSha256.create(&mac, request_id, value); return std.fmt.bytesToHex(mac, .lower); } pub fn validCsrf(request: *const http_api.Request, request_id: []const u8, submitted: []const u8) bool { if (!sameOrigin(request) or submitted.len != 64) return false; const value = token(request) orelse return false; return std.crypto.timing_safe.eql([64]u8, csrf(value, request_id), submitted[0..64].*); } pub fn remember(request: *const http_api.Request, allocator: std.mem.Allocator, user: auth.Account) !void { const value = try store.randomToken(allocator, "", 32); try store.putBrowserSession(&digest(value), user, clock.nowSeconds() + lifetime); if (token(request)) |old| try store.deleteBrowserSession(&digest(old)); const cookie = try std.fmt.allocPrint(allocator, cookie_name ++ "={s}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age={d}", .{ value, lifetime }); try http_api.addResponseHeader("set-cookie", cookie); } pub fn forget(request: *const http_api.Request) !void { if (token(request)) |value| try store.deleteBrowserSession(&digest(value)); try http_api.addResponseHeader("set-cookie", cookie_name ++ "=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0"); } test "browser consent is bound to both browser and authorization request" { try std.testing.expect(!std.mem.eql(u8, &csrf("browser-a", "request-a"), &csrf("browser-b", "request-a"))); try std.testing.expect(!std.mem.eql(u8, &csrf("browser-a", "request-a"), &csrf("browser-a", "request-b"))); }