Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/2.x/security/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"label": "Security",
"position": 5
}
137 changes: 137 additions & 0 deletions docs/2.x/security/authentication.md
Original file line number Diff line number Diff line change
@@ -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`
93 changes: 93 additions & 0 deletions docs/2.x/security/passwords.md
Original file line number Diff line number Diff line change
@@ -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);
```