From 5bda445964a17bb55c9192b559bc6b6015051f42 Mon Sep 17 00:00:00 2001 From: Dhemy Date: Sat, 2 May 2026 15:27:51 +0200 Subject: [PATCH] feat(security): add security documetation - Authentication - Passwords --- docs/2.x/security/_category_.json | 4 + docs/2.x/security/authentication.md | 137 ++++++++++++++++++++++++++++ docs/2.x/security/passwords.md | 93 +++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 docs/2.x/security/_category_.json create mode 100644 docs/2.x/security/authentication.md create mode 100644 docs/2.x/security/passwords.md diff --git a/docs/2.x/security/_category_.json b/docs/2.x/security/_category_.json new file mode 100644 index 0000000..f6d9ef0 --- /dev/null +++ b/docs/2.x/security/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Security", + "position": 5 +} diff --git a/docs/2.x/security/authentication.md b/docs/2.x/security/authentication.md new file mode 100644 index 0000000..fab91e5 --- /dev/null +++ b/docs/2.x/security/authentication.md @@ -0,0 +1,137 @@ +--- +title: Authentication +sidebar_position: 1 +--- + +Authentication in KoalaTs starts at the request boundary. You define which routes are public, which routes require a +user, and how that user is resolved for the request. + +## Define The Security Configuration + +Keep the firewall rules in one place so the security policy is easy to review. + +```ts title="./src/config/security.ts" +import type { SecurityConfig } from '@koala-ts/framework/Security'; +import { userProvider } from './user-provider'; + +export const security: SecurityConfig = { + firewalls: [ + { + pattern: '^/login', + security: false, + }, + { + pattern: '^/api', + security: true, + provider: userProvider, + }, + ], +}; +``` + +In that setup: + +- `/login` stays public +- routes under `/api` require an authenticated user + +If a request matches a protected firewall and no user can be resolved, KoalaTs throws `401 Authentication required`. + +## Apply The Firewall + +Apply the firewall through `globalMiddleware` so it runs before your route handlers. + +```ts title="./src/config/koala.ts" +import type { KoalaConfig } from '@koala-ts/framework'; +import { firewall } from '@koala-ts/framework/Security'; +import { security } from './security'; + +export const appConfig: KoalaConfig = { + globalMiddleware: [firewall(security)], + routes: [], +}; +``` + +## Resolve The Current User + +On protected routes, the firewall can call a `provider` to resolve the current user from the request. + +```ts title="./src/config/user-provider.ts" +import type { UserProvider } from '@koala-ts/framework/Security'; + +export const userProvider: UserProvider = async request => { + if ('Bearer demo-token' !== request.headers.authorization) { + return undefined; // reject the request with 401 Authentication required + } + + return { + id: 'user-1', + name: 'John Doe', + roles: ['ROLE_USER'], + }; +}; +``` + +The provider contract is simple: + +- return a user object to authenticate the request +- return `undefined` to reject the request with `401 Authentication required` + +When a user is returned, KoalaTs assigns it to `scope.user`. + +## Firewall Rule Matching + +KoalaTs uses the first matching firewall rule. Order matters, so place specific rules before broader ones. + +```ts +firewall({ + firewalls: [ + { + pattern: '^/api/public', + security: false, + }, + { + pattern: '^/api', + security: true, + }, + ], +}); +``` + +In that example, `/api/public/health` stays public because the more specific rule matches first. + +Each firewall rule has three parts: + +- `pattern`: a regular expression tested against `scope.request.path` +- `security`: whether the matching path requires authentication +- `provider`: an optional async function that resolves a user from the request + +## Working With `scope.user` + +After the firewall authenticates a request, the resolved user is assigned to `scope.user`. + +That lets downstream middleware and route handlers use the current user without repeating token parsing or lookup logic. + +## Testing Authenticated Requests + +In e2e tests, you can act as a specific user when creating the test agent. + +```ts +import { createTestAgent } from '@koala-ts/framework'; + +const MyUser = { + identifier: 'user-1', +}; + +const agent = createTestAgent(appConfig, { actAs: MyUser }); +``` + +Use a plain agent when you want to assert that a protected route returns `401`: + +```ts +const anonymousAgent = createTestAgent(appConfig); +``` + +This gives you two clear test paths: + +- unauthenticated requests should be rejected on protected routes +- authenticated requests can be executed with `actAs` diff --git a/docs/2.x/security/passwords.md b/docs/2.x/security/passwords.md new file mode 100644 index 0000000..26de23b --- /dev/null +++ b/docs/2.x/security/passwords.md @@ -0,0 +1,93 @@ +--- +title: Passwords +sidebar_position: 2 +--- + +Password hashing is part of the normal account lifecycle: create a hash when a user registers, verify it during login, +and upgrade it later when your hashing policy changes. + +KoalaTs provides `createPasswordHasher(...)` for that flow. + +## Create A Password Hasher + +Start with the default configuration. + +```ts +import { createPasswordHasher } from '@koala-ts/framework/Security'; + +const passwordHasher = createPasswordHasher(); +``` + +This is the recommended starting point. + +The hasher exposes three methods: + +- `hash(plainPassword)` +- `verify(hashedPassword, plainPassword)` +- `needsRehash(hashedPassword)` + +## Configure It If Needed + +If your application has a specific hashing policy, you can pass custom Argon2 options. + +```ts title="password-hasher.ts" +import { createPasswordHasher } from '@koala-ts/framework/Security'; + +export const passwordHasher = createPasswordHasher({ + timeCost: 3, + memoryCost: 32768, +}); +``` + +Available options include: + +- `hashLength` +- `timeCost` +- `memoryCost` +- `parallelism` +- `secret` +- `associatedData` + +Use custom values only when you have a reason to tune them. Most applications should start with the defaults. + +## Hash Passwords + +Use the hasher when storing a new password. + +```ts title="user-service.ts" +import { passwordHasher } from 'password-hasher'; + +const plainPassword = 'my-plain-password'; +const hashedPassword = await passwordHasher.hash(plainPassword); +``` + +Store the returned hash, not the plaintext password. + +## Verify Passwords + +Use the same hasher during login. + +```ts title="auth-service.ts" +import { passwordHasher } from 'password-hasher'; + +const submittedPassword = 'password-submitted-by-user'; +const storedHash = 'argon2id$...'; // retrieved from the database + +// check whether the submitted password matches the stored hash +const valid = await passwordHasher.verify(storedHash, submittedPassword); +``` + +## Rehash Passwords + +When your hashing policy changes, an existing hash may still verify successfully while also needing rehashing. + +That is what `needsRehash(...)` is for. + +```ts +import { passwordHasher } from 'password-hasher'; + +const storedHash = 'argon2id$...'; // retrieved from the database + +// check whether the stored hash needs to be upgraded to match the current policy +const needsUpgrade = passwordHasher.needsRehash(storedHash); +```