Basic password auth

This commit is contained in:
jaina heartles 2022-07-20 22:26:13 -07:00
parent 7b1e4030b0
commit e6217d543d
5 changed files with 40 additions and 8 deletions

View file

@ -135,6 +135,10 @@ pub const ApiServer = struct {
return self.db.getBy(models.User, .id, id, alloc); return self.db.getBy(models.User, .id, id, alloc);
} }
pub fn getUserByHandle(self: *ApiServer, handle: []const u8, alloc: std.mem.Allocator) !?models.User {
return self.db.getBy(models.User, .handle, handle, alloc);
}
pub fn react(self: *ApiServer, note_id: Uuid, ctx: ApiContext) !void { pub fn react(self: *ApiServer, note_id: Uuid, ctx: ApiContext) !void {
try self.db.insert(models.Reaction, .{ .note_id = note_id, .reactor_id = ctx.user_context.user.id }); try self.db.insert(models.Reaction, .{ .note_id = note_id, .reactor_id = ctx.user_context.user.id });
} }

View file

@ -77,10 +77,15 @@ pub fn createNote(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs)
} }
pub fn createUser(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void { pub fn createUser(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
const info = try utils.parseRequestBody(api.CreateInfo(models.User), ctx); const credentials = try utils.parseRequestBody(struct { handle: []const u8, password: []const u8 }, ctx);
defer utils.freeRequestBody(info, ctx.alloc); defer utils.freeRequestBody(credentials, ctx.alloc);
const user = srv.api.createUser(info) catch |err| switch (err) { var hash_buf: [128]u8 = undefined;
const Hash = std.crypto.pwhash.scrypt;
const hashed_password = try Hash.strHash(credentials.password, .{ .allocator = ctx.alloc, .params = Hash.Params.interactive, .encoding = .phc }, &hash_buf);
const user = srv.api.createUser(.{ .handle = credentials.handle, .hashed_password = hashed_password }) catch |err| switch (err) {
error.HandleNotAvailable => return try utils.respondError(ctx, .bad_request, "handle not available"), error.HandleNotAvailable => return try utils.respondError(ctx, .bad_request, "handle not available"),
else => return err, else => return err,
}; };
@ -135,6 +140,22 @@ pub fn authenticate(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs
try utils.respondJson(ctx, .ok, user_ctx.user_context); try utils.respondJson(ctx, .ok, user_ctx.user_context);
} }
pub fn login(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
const credentials = try utils.parseRequestBody(struct { username: []const u8, password: []const u8 }, ctx);
defer utils.freeRequestBody(credentials, ctx.alloc);
// TODO: This gives away the existence of a user through a timing side channel. is that acceptable?
const user = (try srv.api.getUserByHandle(credentials.username, ctx.alloc)) orelse return utils.respondError(ctx, .bad_request, "Invalid Login");
const Hash = std.crypto.pwhash.scrypt;
Hash.strVerify(user.hashed_password, credentials.password, .{ .allocator = ctx.alloc }) catch |err| switch (err) {
error.PasswordVerificationFailed => return utils.respondError(ctx, .bad_request, "Invalid Login"),
else => return err,
};
try utils.respondJson(ctx, .ok, .{ .id = user.id });
}
pub fn healthcheck(_: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void { pub fn healthcheck(_: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
try utils.respondJson(ctx, .ok, .{ .status = "ok" }); try utils.respondJson(ctx, .ok, .{ .status = "ok" });
} }

View file

@ -104,7 +104,9 @@ pub const Database = struct {
\\CREATE TABLE IF NOT EXISTS \\CREATE TABLE IF NOT EXISTS
\\user( \\user(
\\ id TEXT PRIMARY KEY, \\ id TEXT PRIMARY KEY,
\\ handle TEXT NOT NULL \\ handle TEXT NOT NULL,
\\
\\ hashed_password TEXT NOT NULL
\\) STRICT; \\) STRICT;
, ,
\\CREATE TABLE IF NOT EXISTS \\CREATE TABLE IF NOT EXISTS

View file

@ -17,13 +17,15 @@ const router = Router{
Route.new(.GET, "/healthcheck", c.healthcheck), Route.new(.GET, "/healthcheck", c.healthcheck),
Route.new(.GET, "/auth/authenticate", c.authenticate), Route.new(.GET, "/auth/authenticate", c.authenticate),
Route.new(.POST, "/auth/login", c.login),
Route.new(.GET, "/notes/:id", c.getNote),
Route.new(.POST, "/notes", c.createNote), Route.new(.POST, "/notes", c.createNote),
Route.new(.GET, "/notes/:id", c.getNote),
Route.new(.GET, "/notes/:id/reacts", c.listReacts), Route.new(.GET, "/notes/:id/reacts", c.listReacts),
Route.new(.POST, "/notes/:id/reacts", c.react), Route.new(.POST, "/notes/:id/reacts", c.react),
Route.new(.GET, "/users/:id", c.getUser),
Route.new(.POST, "/users", c.createUser), Route.new(.POST, "/users", c.createUser),
Route.new(.GET, "/users/:id", c.getUser),
}, },
}; };
@ -43,8 +45,9 @@ pub const RequestServer = struct {
defer srv.shutdown(); defer srv.shutdown();
while (true) { while (true) {
var buf: [1 << 20]u8 = undefined; const buf = try self.alloc.alloc(u8, 1 << 28); // 4mb
var fba = std.heap.FixedBufferAllocator.init(&buf); defer self.alloc.free(buf);
var fba = std.heap.FixedBufferAllocator.init(buf);
const alloc = fba.allocator(); const alloc = fba.allocator();
var ctx = try srv.accept(alloc); var ctx = try srv.accept(alloc);

View file

@ -15,6 +15,8 @@ pub const Note = struct {
pub const User = struct { pub const User = struct {
id: Uuid, id: Uuid,
handle: []const u8, handle: []const u8,
hashed_password: []const u8, // encoded in PHC format, with salt
}; };
pub const Reaction = struct { pub const Reaction = struct {