Skip to content

Commit

Permalink
Some enhancement of the lockout mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
zefhemel committed Nov 15, 2024
1 parent 046a0df commit 51fc595
Show file tree
Hide file tree
Showing 8 changed files with 57 additions and 52 deletions.
32 changes: 29 additions & 3 deletions cmd/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ import { runPlug } from "../cmd/plug_run.ts";
import { PrefixedKvPrimitives } from "$lib/data/prefixed_kv_primitives.ts";
import { sleep } from "$lib/async.ts";

export type AuthOptions = {
authToken?: string;
user: string;
pass: string;
lockoutTime: number;
lockoutLimit: number;
};

export async function serveCommand(
options: {
hostname?: string;
Expand Down Expand Up @@ -63,10 +71,29 @@ export async function serveCommand(

const userAuth = options.user ?? Deno.env.get("SB_USER");

let userCredentials: { user: string; pass: string } | undefined;
let userCredentials: AuthOptions | undefined;
if (userAuth) {
const [user, pass] = userAuth.split(":");
userCredentials = { user, pass };
userCredentials = {
user,
pass,
// 10 failed login attempts in 1 minute
lockoutLimit: 10,
lockoutTime: 60,
};
// Override lockout settings if they are set in the environment
if (Deno.env.get("SB_LOCKOUT_LIMIT")) {
userCredentials.lockoutLimit = Number(Deno.env.get("SB_LOCKOUT_LIMIT"));
}
if (Deno.env.get("SB_LOCKOUT_TIME")) {
userCredentials.lockoutTime = Number(Deno.env.get("SB_LOCKOUT_TIME"));
}
if (Deno.env.get("SB_AUTH_TOKEN")) {
userCredentials.authToken = Deno.env.get("SB_AUTH_TOKEN");
}
console.log(
`User authentication enabled for user "${user}" with lockout limit ${userCredentials.lockoutLimit} and lockout time ${userCredentials.lockoutTime}s`,
);
}

const backendConfig = Deno.env.get("SB_SHELL_BACKEND") || "local";
Expand Down Expand Up @@ -125,7 +152,6 @@ export async function serveCommand(
certFile: options.cert,

auth: userCredentials,
authToken: Deno.env.get("SB_AUTH_TOKEN"),
syncOnly,
readOnly,
shellBackend: backendConfig,
Expand Down
24 changes: 15 additions & 9 deletions server/http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "@silverbulletmd/silverbullet/lib/page_ref";
import { base64Encode } from "$lib/crypto.ts";
import { LockoutTimer } from "./lockout.ts";
import type { AuthOptions } from "../cmd/server.ts";

const authenticationExpirySeconds = 60 * 60 * 24 * 7; // 1 week

Expand All @@ -32,11 +33,8 @@ export type ServerOptions = {
baseKvPrimitives: KvPrimitives;
certFile?: string;
keyFile?: string;

// Enable username/password auth
auth?: { user: string; pass: string };
// Additional API auth token
authToken?: string;
// Enable username/password/token auth
auth?: AuthOptions;
pagesPath: string;
shellBackend: string;
syncOnly: boolean;
Expand Down Expand Up @@ -315,7 +313,15 @@ export class HttpServer {
"/logo.png",
"/.auth",
];
const lockoutTimer = new LockoutTimer();

// Since we're a single user app, we can use a single lockout timer to prevent brute force attacks
const lockoutTimer = this.options.auth?.lockoutLimit
? new LockoutTimer(
// Turn into ms
this.options.auth.lockoutTime * 1000,
this.options.auth.lockoutLimit!,
)
: new LockoutTimer(0, 0); // disabled

// TODO: This should probably be a POST request
this.app.get("/.logout", (c) => {
Expand Down Expand Up @@ -397,7 +403,7 @@ export class HttpServer {
// Check auth
this.app.use("*", async (c, next) => {
const req = c.req;
if (!this.spaceServer.auth && !this.spaceServer.authToken) {
if (!this.spaceServer.auth) {
// Auth disabled in this config, skip
return next();
}
Expand All @@ -414,12 +420,12 @@ export class HttpServer {
if (!excludedPaths.includes(url.pathname)) {
const authCookie = getCookie(c, authCookieName(host));

if (!authCookie && this.spaceServer.authToken) {
if (!authCookie && this.spaceServer.auth?.authToken) {
// Attempt Bearer Authorization based authentication
const authHeader = req.header("Authorization");
if (authHeader && authHeader.startsWith("Bearer ")) {
const authToken = authHeader.slice("Bearer ".length);
if (authToken === this.spaceServer.authToken) {
if (authToken === this.spaceServer.auth.authToken) {
// All good, let's proceed
this.refreshLogin(c, host);
return next();
Expand Down
27 changes: 3 additions & 24 deletions server/lockout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,14 @@ import { FakeTime } from "@std/testing/time";
const lockoutTime = 60000;
const lockoutLimit = 10;

Deno.test("Lockout - failed login rate limiter", async () => {
const envLockoutTime = Deno.env.get("SB_LOCKOUT_TIME_MS") || "";
const envLockoutLimit = Deno.env.get("SB_LOCKOUT_LIMIT") || "";

Deno.test("Lockout - failed login rate limiter", () => {
using time = new FakeTime();

testDisabled(new LockoutTimer(NaN, lockoutLimit), "by period");
testDisabled(new LockoutTimer(lockoutTime, NaN), "by limit");

Deno.env.set("SB_LOCKOUT_TIME_MS", String(lockoutTime));
Deno.env.set("SB_LOCKOUT_LIMIT", "");
testDisabled(new LockoutTimer(lockoutTime, NaN), "by env period");

Deno.env.set("SB_LOCKOUT_TIME_MS", "");
Deno.env.set("SB_LOCKOUT_LIMIT", String(lockoutLimit));
testDisabled(new LockoutTimer(lockoutTime, NaN), "by env limit");

Deno.env.set("SB_LOCKOUT_TIME_MS", "");
Deno.env.set("SB_LOCKOUT_LIMIT", "");
await testLockout(new LockoutTimer(lockoutTime, 10), "explicit params");
await testLockoutPerMS(new LockoutTimer(lockoutTime, 10), "explicit params");

Deno.env.set("SB_LOCKOUT_TIME_MS", String(lockoutTime));
Deno.env.set("SB_LOCKOUT_LIMIT", String(lockoutLimit));
await testLockout(new LockoutTimer(), "params from env");
await testLockoutPerMS(new LockoutTimer(), "params from env");

Deno.env.set("SB_LOCKOUT_TIME_MS", envLockoutTime);
Deno.env.set("SB_LOCKOUT_LIMIT", envLockoutLimit);
testLockout(new LockoutTimer(lockoutTime, 10), "explicit params");
testLockoutPerMS(new LockoutTimer(lockoutTime, 10), "explicit params");

function testDisabled(timer: LockoutTimer, txt: string) {
for (let i = 0; i < 100; i++) {
Expand Down
4 changes: 2 additions & 2 deletions server/lockout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export class LockoutTimer {
disabled: boolean;

constructor(
countPeriodMs: number = Number(Deno.env.get("SB_LOCKOUT_TIME_MS")) || NaN,
limit: number = Number(Deno.env.get("SB_LOCKOUT_LIMIT")) || NaN,
countPeriodMs: number,
limit: number,
) {
this.disabled = isNaN(countPeriodMs) || isNaN(limit) || countPeriodMs < 1 ||
limit < 1;
Expand Down
7 changes: 3 additions & 4 deletions server/space_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ import {
defaultConfig,
} from "../type/config.ts";
import type { ServerOptions } from "./http_server.ts";
import type { AuthOptions } from "../cmd/server.ts";

// Equivalent of Client on the server
export class SpaceServer implements ConfigContainer {
public pagesPath: string;
auth?: { user: string; pass: string };
authToken?: string;
auth?: AuthOptions;
hostname: string;

config: Config;
Expand All @@ -54,7 +54,6 @@ export class SpaceServer implements ConfigContainer {
this.pagesPath = options.pagesPath;
this.hostname = options.hostname;
this.auth = options.auth;
this.authToken = options.authToken;
this.syncOnly = options.syncOnly;
this.readOnly = options.readOnly;
this.config = defaultConfig;
Expand Down Expand Up @@ -115,7 +114,7 @@ export class SpaceServer implements ConfigContainer {
if (this.auth) {
// Initialize JWT issuer
await this.jwtIssuer.init(
JSON.stringify({ auth: this.auth, authToken: this.authToken }),
JSON.stringify({ auth: this.auth }),
);
}

Expand Down
10 changes: 2 additions & 8 deletions website/Authentication.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
SilverBullet supports simple authentication for a single user.

By simply passing the `--user` flag with a username:password combination, you enable authentication for a single user. For instance:
By passing the `--user` flag with a username:password combination, you enable authentication for a single user. For instance:

```shell
silverbullet --user pete:1234 .
```

Will let `pete` authenticate with password `1234`.

Alternatively, the same information can be passed in via the `SB_USER` environment variable, e.g.

```shell
SB_USER=pete:1234 silverbullet .
```

This is especially convenient when deploying using Docker
Authentication can also be configured via environment variables (which offer a bit more flexibility), see [[Install/Configuration#Authentication]].
3 changes: 1 addition & 2 deletions website/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ An attempt at documenting the changes/new features introduced in each release.

## Edge
_These features are not yet properly released, you need to use [the edge builds](https://community.silverbullet.md/t/living-on-the-edge-builds/27) to try them._
* Nothing yet since 0.10.0. Stay tuned!

[[CHANGELOG]]
* (Security) Implemented a lockout mechanism after a number of failed login attempts for [[Authentication]] (configured via [[Install/Configuration#Authentication]]) (by [Peter Weston](https://github.com/silverbulletmd/silverbullet/pull/1152))

## 0.10.1
This is a “major” release primarily because of the underlying migration to rely on Deno 2 and a bunch of foundational work that’s not really leveraged yet. Stay tuned for more.
Expand Down
2 changes: 2 additions & 0 deletions website/Install/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ SilverBullet supports basic authentication for a single user.

* `SB_USER`: Sets single-user credentials, e.g. `SB_USER=pete:1234` allows you to login with username “pete” and password “1234”.
* `SB_AUTH_TOKEN`: Enables `Authorization: Bearer <token>` style authentication on the [[API]] (useful for [[Sync]] and remote HTTP storage backends).
* `SB_LOCKOUT_LIMIT`: Specifies the number of failed login attempt before locking the user out (for a `SB_LOCKOUT_TIME` specified amount of seconds), defaults to `10`
* `SB_LOCKOUT_TIME`: Specifies the amount of time (in seconds) a client will be blocked until attempting to log back in.

# Storage
SilverBullet supports multiple storage backends for keeping your [[Spaces]] content.
Expand Down

0 comments on commit 51fc595

Please sign in to comment.