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 { PrefixedKvPrimitives } from "$lib/data/prefixed_kv_primitives.ts";
|
||||||
import { sleep } from "$lib/async.ts";
|
import { sleep } from "$lib/async.ts";
|
||||||
|
|
||||||
|
export type AuthOptions = {
|
||||||
|
authToken?: string;
|
||||||
|
user: string;
|
||||||
|
pass: string;
|
||||||
|
lockoutTime: number;
|
||||||
|
lockoutLimit: number;
|
||||||
|
};
|
||||||
|
|
||||||
export async function serveCommand(
|
export async function serveCommand(
|
||||||
options: {
|
options: {
|
||||||
hostname?: string;
|
hostname?: string;
|
||||||
|
@ -63,10 +71,29 @@ export async function serveCommand(
|
||||||
|
|
||||||
const userAuth = options.user ?? Deno.env.get("SB_USER");
|
const userAuth = options.user ?? Deno.env.get("SB_USER");
|
||||||
|
|
||||||
let userCredentials: { user: string; pass: string } | undefined;
|
let userCredentials: AuthOptions | undefined;
|
||||||
if (userAuth) {
|
if (userAuth) {
|
||||||
const [user, pass] = userAuth.split(":");
|
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";
|
const backendConfig = Deno.env.get("SB_SHELL_BACKEND") || "local";
|
||||||
|
@ -125,7 +152,6 @@ export async function serveCommand(
|
||||||
certFile: options.cert,
|
certFile: options.cert,
|
||||||
|
|
||||||
auth: userCredentials,
|
auth: userCredentials,
|
||||||
authToken: Deno.env.get("SB_AUTH_TOKEN"),
|
|
||||||
syncOnly,
|
syncOnly,
|
||||||
readOnly,
|
readOnly,
|
||||||
shellBackend: backendConfig,
|
shellBackend: backendConfig,
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
} from "@silverbulletmd/silverbullet/lib/page_ref";
|
} from "@silverbulletmd/silverbullet/lib/page_ref";
|
||||||
import { base64Encode } from "$lib/crypto.ts";
|
import { base64Encode } from "$lib/crypto.ts";
|
||||||
import { LockoutTimer } from "./lockout.ts";
|
import { LockoutTimer } from "./lockout.ts";
|
||||||
|
import type { AuthOptions } from "../cmd/server.ts";
|
||||||
|
|
||||||
const authenticationExpirySeconds = 60 * 60 * 24 * 7; // 1 week
|
const authenticationExpirySeconds = 60 * 60 * 24 * 7; // 1 week
|
||||||
|
|
||||||
|
@ -32,11 +33,8 @@ export type ServerOptions = {
|
||||||
baseKvPrimitives: KvPrimitives;
|
baseKvPrimitives: KvPrimitives;
|
||||||
certFile?: string;
|
certFile?: string;
|
||||||
keyFile?: string;
|
keyFile?: string;
|
||||||
|
// Enable username/password/token auth
|
||||||
// Enable username/password auth
|
auth?: AuthOptions;
|
||||||
auth?: { user: string; pass: string };
|
|
||||||
// Additional API auth token
|
|
||||||
authToken?: string;
|
|
||||||
pagesPath: string;
|
pagesPath: string;
|
||||||
shellBackend: string;
|
shellBackend: string;
|
||||||
syncOnly: boolean;
|
syncOnly: boolean;
|
||||||
|
@ -315,7 +313,15 @@ export class HttpServer {
|
||||||
"/logo.png",
|
"/logo.png",
|
||||||
"/.auth",
|
"/.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
|
// TODO: This should probably be a POST request
|
||||||
this.app.get("/.logout", (c) => {
|
this.app.get("/.logout", (c) => {
|
||||||
|
@ -397,7 +403,7 @@ export class HttpServer {
|
||||||
// Check auth
|
// Check auth
|
||||||
this.app.use("*", async (c, next) => {
|
this.app.use("*", async (c, next) => {
|
||||||
const req = c.req;
|
const req = c.req;
|
||||||
if (!this.spaceServer.auth && !this.spaceServer.authToken) {
|
if (!this.spaceServer.auth) {
|
||||||
// Auth disabled in this config, skip
|
// Auth disabled in this config, skip
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
@ -414,12 +420,12 @@ export class HttpServer {
|
||||||
if (!excludedPaths.includes(url.pathname)) {
|
if (!excludedPaths.includes(url.pathname)) {
|
||||||
const authCookie = getCookie(c, authCookieName(host));
|
const authCookie = getCookie(c, authCookieName(host));
|
||||||
|
|
||||||
if (!authCookie && this.spaceServer.authToken) {
|
if (!authCookie && this.spaceServer.auth?.authToken) {
|
||||||
// Attempt Bearer Authorization based authentication
|
// Attempt Bearer Authorization based authentication
|
||||||
const authHeader = req.header("Authorization");
|
const authHeader = req.header("Authorization");
|
||||||
if (authHeader && authHeader.startsWith("Bearer ")) {
|
if (authHeader && authHeader.startsWith("Bearer ")) {
|
||||||
const authToken = authHeader.slice("Bearer ".length);
|
const authToken = authHeader.slice("Bearer ".length);
|
||||||
if (authToken === this.spaceServer.authToken) {
|
if (authToken === this.spaceServer.auth.authToken) {
|
||||||
// All good, let's proceed
|
// All good, let's proceed
|
||||||
this.refreshLogin(c, host);
|
this.refreshLogin(c, host);
|
||||||
return next();
|
return next();
|
||||||
|
|
|
@ -5,35 +5,14 @@ import { FakeTime } from "@std/testing/time";
|
||||||
const lockoutTime = 60000;
|
const lockoutTime = 60000;
|
||||||
const lockoutLimit = 10;
|
const lockoutLimit = 10;
|
||||||
|
|
||||||
Deno.test("Lockout - failed login rate limiter", async () => {
|
Deno.test("Lockout - failed login rate limiter", () => {
|
||||||
const envLockoutTime = Deno.env.get("SB_LOCKOUT_TIME_MS") || "";
|
|
||||||
const envLockoutLimit = Deno.env.get("SB_LOCKOUT_LIMIT") || "";
|
|
||||||
|
|
||||||
using time = new FakeTime();
|
using time = new FakeTime();
|
||||||
|
|
||||||
testDisabled(new LockoutTimer(NaN, lockoutLimit), "by period");
|
testDisabled(new LockoutTimer(NaN, lockoutLimit), "by period");
|
||||||
testDisabled(new LockoutTimer(lockoutTime, NaN), "by limit");
|
testDisabled(new LockoutTimer(lockoutTime, NaN), "by limit");
|
||||||
|
|
||||||
Deno.env.set("SB_LOCKOUT_TIME_MS", String(lockoutTime));
|
testLockout(new LockoutTimer(lockoutTime, 10), "explicit params");
|
||||||
Deno.env.set("SB_LOCKOUT_LIMIT", "");
|
testLockoutPerMS(new LockoutTimer(lockoutTime, 10), "explicit params");
|
||||||
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);
|
|
||||||
|
|
||||||
function testDisabled(timer: LockoutTimer, txt: string) {
|
function testDisabled(timer: LockoutTimer, txt: string) {
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
|
|
|
@ -11,8 +11,8 @@ export class LockoutTimer {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
countPeriodMs: number = Number(Deno.env.get("SB_LOCKOUT_TIME_MS")) || NaN,
|
countPeriodMs: number,
|
||||||
limit: number = Number(Deno.env.get("SB_LOCKOUT_LIMIT")) || NaN,
|
limit: number,
|
||||||
) {
|
) {
|
||||||
this.disabled = isNaN(countPeriodMs) || isNaN(limit) || countPeriodMs < 1 ||
|
this.disabled = isNaN(countPeriodMs) || isNaN(limit) || countPeriodMs < 1 ||
|
||||||
limit < 1;
|
limit < 1;
|
||||||
|
|
|
@ -25,12 +25,12 @@ import {
|
||||||
defaultConfig,
|
defaultConfig,
|
||||||
} from "../type/config.ts";
|
} from "../type/config.ts";
|
||||||
import type { ServerOptions } from "./http_server.ts";
|
import type { ServerOptions } from "./http_server.ts";
|
||||||
|
import type { AuthOptions } from "../cmd/server.ts";
|
||||||
|
|
||||||
// Equivalent of Client on the server
|
// Equivalent of Client on the server
|
||||||
export class SpaceServer implements ConfigContainer {
|
export class SpaceServer implements ConfigContainer {
|
||||||
public pagesPath: string;
|
public pagesPath: string;
|
||||||
auth?: { user: string; pass: string };
|
auth?: AuthOptions;
|
||||||
authToken?: string;
|
|
||||||
hostname: string;
|
hostname: string;
|
||||||
|
|
||||||
config: Config;
|
config: Config;
|
||||||
|
@ -54,7 +54,6 @@ export class SpaceServer implements ConfigContainer {
|
||||||
this.pagesPath = options.pagesPath;
|
this.pagesPath = options.pagesPath;
|
||||||
this.hostname = options.hostname;
|
this.hostname = options.hostname;
|
||||||
this.auth = options.auth;
|
this.auth = options.auth;
|
||||||
this.authToken = options.authToken;
|
|
||||||
this.syncOnly = options.syncOnly;
|
this.syncOnly = options.syncOnly;
|
||||||
this.readOnly = options.readOnly;
|
this.readOnly = options.readOnly;
|
||||||
this.config = defaultConfig;
|
this.config = defaultConfig;
|
||||||
|
@ -115,7 +114,7 @@ export class SpaceServer implements ConfigContainer {
|
||||||
if (this.auth) {
|
if (this.auth) {
|
||||||
// Initialize JWT issuer
|
// Initialize JWT issuer
|
||||||
await this.jwtIssuer.init(
|
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.
|
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
|
```shell
|
||||||
silverbullet --user pete:1234 .
|
silverbullet --user pete:1234 .
|
||||||
|
@ -8,10 +8,4 @@ silverbullet --user pete:1234 .
|
||||||
|
|
||||||
Will let `pete` authenticate with password `1234`.
|
Will let `pete` authenticate with password `1234`.
|
||||||
|
|
||||||
Alternatively, the same information can be passed in via the `SB_USER` environment variable, e.g.
|
Authentication can also be configured via environment variables (which offer a bit more flexibility), see [[Install/Configuration#Authentication]].
|
||||||
|
|
||||||
```shell
|
|
||||||
SB_USER=pete:1234 silverbullet .
|
|
||||||
```
|
|
||||||
|
|
||||||
This is especially convenient when deploying using Docker
|
|
||||||
|
|
|
@ -4,9 +4,8 @@ An attempt at documenting the changes/new features introduced in each release.
|
||||||
|
|
||||||
## Edge
|
## 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._
|
_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
|
## 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.
|
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_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_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
|
# Storage
|
||||||
SilverBullet supports multiple storage backends for keeping your [[Spaces]] content.
|
SilverBullet supports multiple storage backends for keeping your [[Spaces]] content.
|
||||||
|
|
Loading…
Reference in New Issue