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 {
|
const services = struct {
|
||||||
pub const communities = @import("./services/communities.zig");
|
pub const communities = @import("./services/communities.zig");
|
||||||
pub const actors = @import("./services/actors.zig");
|
pub const actors = @import("./services/actors.zig");
|
||||||
pub const auth = @import("./services/auth.zig");
|
|
||||||
pub const drive = @import("./services/drive.zig");
|
pub const drive = @import("./services/drive.zig");
|
||||||
pub const files = @import("./services/files.zig");
|
pub const files = @import("./services/files.zig");
|
||||||
pub const invites = @import("./services/invites.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(),
|
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);
|
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 {
|
pub fn connectToken(self: *ApiSource, host: []const u8, token: []const u8, alloc: std.mem.Allocator) !Conn {
|
||||||
const db = try self.db_conn_pool.acquire();
|
var conn = try self.connectUnauthorized(host, alloc);
|
||||||
errdefer db.releaseConnection();
|
errdefer conn.close();
|
||||||
const community = try services.communities.getByHost(db, host, alloc);
|
conn.context.token_info = try conn.verifyToken(token);
|
||||||
|
return conn;
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,8 @@ pub fn methods(comptime models: type) type {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn createLocalAccount(
|
// Only for internal use
|
||||||
|
pub fn createLocalAccount(
|
||||||
db: anytype,
|
db: anytype,
|
||||||
username: []const u8,
|
username: []const u8,
|
||||||
password: []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,
|
username: []const u8,
|
||||||
password: []const u8,
|
password: []const u8,
|
||||||
community_id: Uuid,
|
community_id: Uuid,
|
||||||
_: @import("../services/auth.zig").RegistrationOptions,
|
_: AccountCreateOptions,
|
||||||
_: std.mem.Allocator,
|
_: std.mem.Allocator,
|
||||||
) anyerror!Uuid {
|
) anyerror!Uuid {
|
||||||
try std.testing.expectEqual(db.tx_level, 1);
|
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