aRustyDev / aRustyDev/mdbook-htmx

docs(adr): ADR-0008: Separate Authentication and Authorization Configuration

Open
#16 0 comments 0 reactions 1 assignee Claimed by @aRustyDev View on GitHub
documentation
Dominant language
Rust
Stars
0
Forks
1
PR merge metrics
No merged PRs in 30d

Description

# ADR-0008: Separate Authentication and Authorization Configuration

## Status

Accepted

## Context

Documentation sites with access control need both:

1. **Authentication (authn)** - Verifying user identity ("Who are you?")
2. **Authorization (authz)** - Checking permissions ("What can you access?")

The original design conflated these into a single `[output.htmx.auth]` section. This ADR addresses why they should be separate.

## Decision Drivers

1. **Public Sign-In Pages** - Login/register pages must be accessible without authentication
2. **Anonymous Users** - Some pages are public; anonymous users should see them
3. **Different Providers** - Authentication and authorization may use different systems
4. **Separation of Concerns** - Identity verification ≠ permission checking

## User Flow

```asciidoc
┌─────────────────────────────────────────────────────────────────────┐
│ User Request │
└─────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────┐
│ Is page public? │
│ (authz.default-access) │
└──────────────────────────────┘
│ │
Yes No
│ │
▼ ▼
┌──────────┐ ┌──────────────────────────┐
│ Serve │ │ Is user authenticated? │
│ Page │ │ (authn check) │
└──────────┘ └──────────────────────────┘
│ │
Yes No
│ │
▼ ▼
┌─────────────────┐ ┌────────────────┐
│ Is user │ │ Redirect to │
│ authorized? │ │ signin partial │
│ (authz check) │ │ (authn.signin) │
└─────────────────┘ └────────────────┘
│ │
Yes No
│ │
▼ ▼
┌──────────┐ ┌──────────────────┐
│ Serve │ │ Show access- │
│ Page │ │ denied partial │
└──────────┘ └──────────────────┘
```

## Decision

**Separate `[output.htmx.authn]` and `[output.htmx.authz]` configuration sections.**

### Configuration Schema

```toml
# Authentication configuration
[output.htmx.authn]
enabled = true
signin-page = "/auth/signin" # Public sign-in page path
signin-partial = "partials/signin.html"
signout-page = "/auth/signout"
session-header = "X-User-Session" # Header containing session token
user-header = "X-User-ID" # Header containing user identity

# Provider hint (for documentation/reference implementation)
provider = "jwt" # jwt | session | oauth2 | saml

# Authorization configuration
[output.htmx.authz]
enabled = true
frontmatter-key = "auth" # Frontmatter key for authz rules
default-access = "public" # public | authenticated | roles
denied-partial = "partials/access-denied.html"

# Provider hint (for documentation/reference implementation)
provider = "manifest" # manifest | spicedb | openfga | opa
```

### Frontmatter Schema

Pages declare authorization requirements, not authentication:

```yaml
---
title: Admin Settings
auth:
access: roles # public | authenticated | roles
roles: [admin, editor] # Required roles (if access == roles)
fallback: /docs/access-denied
---
```

### Manifest Schema

```json
{
"authn": {
"signin": "/auth/signin",
"signout": "/auth/signout"
},
"pages": [
{
"path": "/docs/admin",
"auth": {
"access": "roles",
"roles": ["admin"]
}
},
{
"path": "/auth/signin",
"auth": {
"access": "public"
}
}
]
}
```

## Implementation Pattern

### Server Middleware

```typescript
// Separate middleware for authn and authz

// 1. Authentication middleware (runs first)
app.use("*", async (c, next) => {
const session = c.req.header("X-User-Session");

if (session) {
const user = await validateSession(session);
c.set("user", user);
}
// Don't block - let authz decide if user is required

await next();
});

// 2. Authorization middleware
app.use("/docs/*", async (c, next) => {
const page = manifest.pages.find(p => p.path === c.req.path);
const user = c.get("user");

// Public pages - allow everyone
if (page.auth.access === "public") {
return next();
}

// Authenticated pages - require any logged-in user
if (page.auth.access === "authenticated") {
if (!user) {
return redirectToSignin(c, page);
}
return next();
}

// Role-based pages - require specific roles
if (page.auth.access === "roles") {
if (!user) {
return redirectToSignin(c, page);
}
if (!hasRequiredRoles(user, page.auth.roles)) {
return showAccessDenied(c, page);
}
return next();
}
});

function redirectToSignin(c: Context, page: Page) {
const returnUrl = encodeURIComponent(c.req.path);

if (isHtmxRequest(c.req)) {
// Return signin partial with OOB swap
return c.html(`


${signinPartial}

`);
}

return c.redirect(`${manifest.authn.signin}?return=${returnUrl}`);
}
```

### Sign-In Partial

```html


```

## Why Two Sections?

| Concern | authn | authz |
|---------|-------|-------|
| **Question** | Who is this user? | Can this user access this page? |
| **When** | Every request (optionally) | Only for protected pages |
| **Provider** | IdP (Okta, Auth0, AD) | Policy engine or manifest |
| **Failure** | Redirect to sign-in | Show access-denied |
| **Pages affected** | Sign-in/out pages special | All pages have access rules |

## Consequences

### Positive
- Clear separation of identity vs. permission
- Sign-in pages naturally public without special cases
- Different providers for authn (Okta) and authz (OpenFGA)
- Anonymous browsing supported (public pages work)
- Composable middleware

### Negative
- Two configuration sections instead of one
- Must coordinate authn and authz providers
- More complex mental model

### Mitigation
- Document the flow clearly (see diagram above)
- Provide reference implementations
- Sensible defaults (`authn.enabled = false`, `authz.default-access = "public"`)

## References

- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
- [OWASP Authorization Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html)
- [NIST Digital Identity Guidelines](https://pages.nist.gov/800-63-3/)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.