From 41ce5f3001cac2a0a6aa661fe5793ef3761d4941 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sun, 1 Jan 2023 17:32:17 -0800 Subject: [PATCH] Remove services/auth.zig --- src/api/lib.zig | 33 ++--- src/api/methods/auth.zig | 27 +++- src/api/services/auth.zig | 261 -------------------------------------- 3 files changed, 37 insertions(+), 284 deletions(-) delete mode 100644 src/api/services/auth.zig diff --git a/src/api/lib.zig b/src/api/lib.zig index 3cbb798..30a2b58 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -10,7 +10,6 @@ const default_avatar = "static/default_avi.png"; const services = struct { pub const communities = @import("./services/communities.zig"); pub const actors = @import("./services/actors.zig"); - pub const auth = @import("./services/auth.zig"); pub const drive = @import("./services/drive.zig"); pub const files = @import("./services/files.zig"); pub const invites = @import("./services/invites.zig"); @@ -258,7 +257,14 @@ pub fn setupAdmin(db: sql.Db, origin: []const u8, username: []const u8, password arena.allocator(), ); - const user = try services.auth.register(tx, username, password, community_id, .{ .kind = .admin }, arena.allocator()); + const user = try @import("./methods/auth.zig").methods(services).createLocalAccount( + tx, + username, + password, + community_id, + .{ .role = .admin }, + arena.allocator(), + ); try services.communities.transferOwnership(tx, community_id, user); @@ -298,25 +304,10 @@ pub const ApiSource = struct { } pub fn connectToken(self: *ApiSource, host: []const u8, token: []const u8, alloc: std.mem.Allocator) !Conn { - const db = try self.db_conn_pool.acquire(); - errdefer db.releaseConnection(); - const community = try services.communities.getByHost(db, host, alloc); - - const token_info = try services.auth.verifyToken( - db, - token, - community.id, - alloc, - ); - - return Conn{ - .db = db, - .context = .{ - .community = community, - .token_info = token_info, - }, - .allocator = alloc, - }; + var conn = try self.connectUnauthorized(host, alloc); + errdefer conn.close(); + conn.context.token_info = try conn.verifyToken(token); + return conn; } }; diff --git a/src/api/methods/auth.zig b/src/api/methods/auth.zig index ade047e..3ec9e11 100644 --- a/src/api/methods/auth.zig +++ b/src/api/methods/auth.zig @@ -69,7 +69,8 @@ pub fn methods(comptime models: type) type { return user; } - fn createLocalAccount( + // Only for internal use + pub fn createLocalAccount( db: anytype, username: []const u8, password: []const u8, @@ -141,6 +142,28 @@ pub fn methods(comptime models: type) type { }, }; } + + pub fn verifyToken(self: anytype, token: []const u8) !Token.Info { + const hash = try hashToken(token, self.allocator); + defer self.allocator.free(hash); + + return self.db.queryRow( + Token.Info, + \\SELECT token.account_id as user_id, token.issued_at + \\FROM token + \\ JOIN account + \\ JOIN actor + \\ ON token.account_id = account.id AND account.id = actor.id + \\WHERE token.hash = $1 AND actor.community_id = $2 + \\LIMIT 1 + , + .{ hash, self.context.community.id }, + self.allocator, + ) catch |err| switch (err) { + error.NoRows => error.InvalidToken, + else => error.DatabaseFailure, + }; + } }; } @@ -275,7 +298,7 @@ test "register" { username: []const u8, password: []const u8, community_id: Uuid, - _: @import("../services/auth.zig").RegistrationOptions, + _: AccountCreateOptions, _: std.mem.Allocator, ) anyerror!Uuid { try std.testing.expectEqual(db.tx_level, 1); diff --git a/src/api/services/auth.zig b/src/api/services/auth.zig deleted file mode 100644 index b0f8200..0000000 --- a/src/api/services/auth.zig +++ /dev/null @@ -1,261 +0,0 @@ -const std = @import("std"); -const util = @import("util"); -const actors = @import("./actors.zig"); -const types = @import("../types.zig"); -const Token = types.Token; - -const Uuid = util.Uuid; -const DateTime = util.DateTime; - -pub const RegistrationError = error{ - PasswordTooShort, - DatabaseFailure, - HashFailure, - OutOfMemory, -} || actors.CreateError; - -pub const min_password_chars = 12; -pub const Kind = enum { - user, - admin, -}; -pub const RegistrationOptions = struct { - invite_id: ?Uuid = null, - email: ?[]const u8 = null, - kind: Kind = .user, -}; - -/// Creates a local account with the given information and returns the -/// account id -pub fn register( - db: anytype, - username: []const u8, - password: []const u8, - community_id: Uuid, - options: RegistrationOptions, - alloc: std.mem.Allocator, -) RegistrationError!Uuid { - if (password.len < min_password_chars) return error.PasswordTooShort; - - // perform pre-validation to avoid having to hash the password if it fails - try actors.validateUsername(username, false); - const hash = try hashPassword(password, alloc); - defer alloc.free(hash); - - const tx = db.beginOrSavepoint() catch return error.DatabaseFailure; - errdefer tx.rollback(); - - const id = try actors.create(tx, username, community_id, false, alloc); - tx.insert("account", .{ - .id = id, - .invite_id = options.invite_id, - .email = options.email, - .kind = options.kind, - }, alloc) catch return error.DatabaseFailure; - tx.insert("password", .{ - .account_id = id, - .hash = hash, - .changed_at = DateTime.now(), - }, alloc) catch return error.DatabaseFailure; - tx.insert("drive_entry", .{ - .id = id, - .owner_id = id, - }, alloc) catch return error.DatabaseFailure; - - tx.commitOrRelease() catch return error.DatabaseFailure; - - return id; -} - -pub const LoginError = error{ - InvalidLogin, - HashFailure, - DatabaseFailure, - OutOfMemory, -}; - -pub const LoginResult = struct { - token: []const u8, - user_id: Uuid, -}; - -/// Attempts to login to the account `@username@community` and creates -/// a login token/cookie for the user -pub fn login( - db: anytype, - username: []const u8, - community_id: Uuid, - password: []const u8, - alloc: std.mem.Allocator, -) LoginError!Token { - std.log.debug("user: {s}, community_id: {}", .{ username, community_id }); - const info = db.queryRow( - struct { account_id: Uuid, hash: []const u8 }, - \\SELECT account.id as account_id, password.hash - \\FROM password - \\ JOIN account - \\ JOIN actor - \\ ON password.account_id = account.id AND account.id = actor.id - \\WHERE actor.username = $1 - \\ AND actor.community_id = $2 - \\LIMIT 1 - , - .{ username, community_id }, - alloc, - ) catch |err| return switch (err) { - error.NoRows => error.InvalidLogin, - else => error.DatabaseFailure, - }; - defer alloc.free(info.hash); - - try verifyPassword(info.hash, password, alloc); - - const token = try generateToken(alloc); - errdefer util.deepFree(alloc, token); - const token_hash = hashToken(token, alloc) catch |err| switch (err) { - error.OutOfMemory => return error.OutOfMemory, - else => unreachable, - }; - defer util.deepFree(alloc, token_hash); - - const tx = db.begin() catch return error.DatabaseFailure; - errdefer tx.rollback(); - - // ensure that the password has not changed in the meantime - { - const updated_info = tx.queryRow( - struct { hash: []const u8 }, - \\SELECT hash - \\FROM password - \\WHERE account_id = $1 - \\LIMIT 1 - , - .{info.account_id}, - alloc, - ) catch return error.DatabaseFailure; - defer util.deepFree(alloc, updated_info); - - if (!std.mem.eql(u8, info.hash, updated_info.hash)) return error.InvalidLogin; - } - - const now = DateTime.now(); - tx.insert("token", .{ - .account_id = info.account_id, - .hash = token_hash, - .issued_at = now, - }, alloc) catch return error.DatabaseFailure; - - tx.commit() catch return error.DatabaseFailure; - - return Token{ - .value = token, - .info = .{ - .user_id = info.account_id, - .issued_at = now, - }, - }; -} - -pub const VerifyTokenError = error{ InvalidToken, DatabaseFailure, OutOfMemory }; -pub fn verifyToken( - db: anytype, - token: []const u8, - community_id: Uuid, - alloc: std.mem.Allocator, -) VerifyTokenError!Token.Info { - const hash = try hashToken(token, alloc); - defer alloc.free(hash); - - return db.queryRow( - Token.Info, - \\SELECT token.account_id as user_id, token.issued_at - \\FROM token - \\ JOIN account - \\ JOIN actor - \\ ON token.account_id = account.id AND account.id = actor.id - \\WHERE token.hash = $1 AND actor.community_id = $2 - \\LIMIT 1 - , - .{ hash, community_id }, - alloc, - ) catch |err| switch (err) { - error.NoRows => error.InvalidToken, - else => error.DatabaseFailure, - }; -} - -// We use scrypt, a password hashing algorithm that attempts to slow down -// GPU-based cracking approaches by using large amounts of memory, for -// password hashing. -// Attempting to calculate/verify a hash will use about 50mb of work space. -const scrypt = std.crypto.pwhash.scrypt; -const password_hash_len = 128; -fn verifyPassword( - hash: []const u8, - password: []const u8, - alloc: std.mem.Allocator, -) LoginError!void { - scrypt.strVerify( - hash, - password, - .{ .allocator = alloc }, - ) catch |err| return switch (err) { - error.PasswordVerificationFailed => error.InvalidLogin, - else => error.HashFailure, - }; -} - -fn hashPassword(password: []const u8, alloc: std.mem.Allocator) ![]const u8 { - const buf = try alloc.alloc(u8, password_hash_len); - errdefer alloc.free(buf); - return scrypt.strHash( - password, - .{ - .allocator = alloc, - .params = scrypt.Params.interactive, - .encoding = .phc, - }, - buf, - ) catch error.HashFailure; -} - -/// A raw token is a sequence of N random bytes, base64 encoded. -/// When the token is generated: -/// - The hash of the token is calculated by: -/// 1. Decoding the base64 text -/// 2. Calculating the SHA256 hash of this text -/// 3. Encoding the hash back as base64 -/// - The b64 encoded hash is stored in the database -/// - The original token is returned to the user -/// * The user will treat it as opaque text -/// When the token is verified: -/// - The hash of the token is taken as shown above -/// - The database is scanned for a token matching this hash -/// - If none can be found, the token is invalid -const Sha256 = std.crypto.hash.sha2.Sha256; -const Base64Encoder = std.base64.standard.Encoder; -const Base64Decoder = std.base64.standard.Decoder; -const token_len = 12; -fn generateToken(alloc: std.mem.Allocator) ![]const u8 { - var token = std.mem.zeroes([token_len]u8); - std.crypto.random.bytes(&token); - - const token_b64_len = Base64Encoder.calcSize(token.len); - const token_b64 = try alloc.alloc(u8, token_b64_len); - return Base64Encoder.encode(token_b64, &token); -} - -fn hashToken(token_b64: []const u8, alloc: std.mem.Allocator) ![]const u8 { - const decoded_token_len = Base64Decoder.calcSizeForSlice(token_b64) catch return error.InvalidToken; - if (decoded_token_len != token_len) return error.InvalidToken; - - var token = std.mem.zeroes([token_len]u8); - Base64Decoder.decode(&token, token_b64) catch return error.InvalidToken; - - var hash = std.mem.zeroes([Sha256.digest_length]u8); - Sha256.hash(&token, &hash, .{}); - - const hash_b64_len = Base64Encoder.calcSize(hash.len); - const hash_b64 = try alloc.alloc(u8, hash_b64_len); - return Base64Encoder.encode(hash_b64, &hash); -}