Some enhancement of the lockout mechanism
parent
046a0df868
commit
51fc5952bc
|
@ -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;
|
||||
|
@ -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";
|
||||
|
@ -125,7 +152,6 @@ export async function serveCommand(
|
|||
certFile: options.cert,
|
||||
|
||||
auth: userCredentials,
|
||||
authToken: Deno.env.get("SB_AUTH_TOKEN"),
|
||||
syncOnly,
|
||||
readOnly,
|
||||
shellBackend: backendConfig,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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) => {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
|
|
|
@ -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++) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 }),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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 .
|
||||
|
@ -8,10 +8,4 @@ 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]].
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue