Remove services/auth.zig
This commit is contained in:
parent
df9354d91f
commit
41ce5f3001
3 changed files with 37 additions and 284 deletions
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
Loading…
Reference in a new issue