Logo Microsoft
Auth & SécuritéSûr9393/100

Auth Tls Patterns (Microsoft)

Use when your service needs authentication that works without friction locally but secures remote access, automatic TLS certificate setup, or token-based auth with auto-generation and localhost bypass.

ou envoie-le directement à ton agent.

Installer dans ton projet

$ curl -fsSL https://arboristhree.xyz/cli/install-skill.mjs -o /tmp/arboris-install-skill.mjs && node /tmp/arboris-install-skill.mjs auth-tls-patterns__microsoft-amplifier-bundle-skills__skills-db43b7bff964
Logo Microsoft
Microsoft
microsoft/amplifier-bundle-skills
Contenu à copier
---
name: auth-tls-patterns
description: >
  Use when your service needs authentication that works without friction
  locally but secures remote access, automatic TLS certificate setup, or
  token-based auth with auto-generation and localhost bypass.
metadata:
  origin: Microsoft
---

# Authentication & TLS

## The Pattern

**Problem:** Your tool serves a web UI. Locally it should just work — no passwords, no login screens. Remotely it needs real authentication and HTTPS. You don't want to configure either manually.

**Approach:** Socket-level localhost bypass (unforgeable), cascading auth strategies with auto-generation, and a TLS setup cascade that picks the best available method automatically.

Pattern proven in production across multiple Python CLI tools and web services.

## Key Design Decisions

### 1. Localhost bypass — socket-level IP, not headers

The single most important auth decision: localhost connections skip all auth checks. But you MUST use the socket-level client IP, not HTTP headers:

```python
_LOCALHOST_ADDRS = {"127.0.0.1", "::1"}

async def dispatch(self, request: Request, call_next) -> Response:
    # client.host is the socket-level IP — cannot be forged by the client
    client_host = request.client.host if request.client else ""
    if client_host in _LOCALHOST_ADDRS:
        return await call_next(request)
```

This is unforgeable — unlike `X-Forwarded-For` or the `Host` header, `request.client.host` comes from the TCP connection's source address. A remote attacker cannot set it to `127.0.0.1`.

A simpler approach checks at the CLI level:

```python
auth_required = resolved_host != "127.0.0.1" and not no_auth
```

### 2. Auth cascade: PAM > password file > auto-generate

Resolve auth mode through a fallback chain:

```python
def _resolve_auth() -> tuple[str, str]:
    """Fallback chain for non-localhost:
      1. PAM available → ("pam", "")
      2. MY_TOOL_PASSWORD env → ("password", <env value>)
      3. ~/.config/my-tool/password file → ("password", <file value>)
      4. Auto-generate → ("password", <generated>)
    """
```

Auto-generation writes a random password to a file with restricted permissions:

```python
def generate_and_save_password() -> str:
    pw = secrets.token_urlsafe(20)
    path = get_password_path()
    _config_dir()  # ensures dir exists with mode 0700
    path.write_text(pw + "\n")
    path.chmod(0o600)
    return pw
```

### 3. Token-based auth with auto-generation

A simpler bearer token approach:

```python
def ensure_token() -> str:
    """Return the existing auth token or generate and persist a new one."""
    if TOKEN_FILE.exists():
        token = TOKEN_FILE.read_text().strip()
        if token:
            return token
    token = secrets.token_urlsafe(32)
    TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
    TOKEN_FILE.write_text(token + "\n")
    TOKEN_FILE.chmod(0o600)
    return token
```

The middleware checks Bearer tokens on protected paths:

```python
class AuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        if not request.app.state.auth_required:
            return await call_next(request)
        path = request.url.path
        if not _is_protected(path):
            return await call_next(request)
        auth_header = request.headers.get("authorization", "")
        if auth_header.startswith("Bearer "):
            token = auth_header[7:]
            if token == request.app.state.auth_token:
                return await call_next(request)
        return JSONResponse(status_code=401, content={"detail": "Unauthorized"})
```

### 4. TLS cascade: Tailscale > mkcert > self-signed

Implement a three-tier TLS cascade, trying the best option first:

```
Tailscale available + cert domains? → Real Let's Encrypt cert, auto-renewed
mkcert installed?                   → Locally-trusted cert, no browser warnings
Neither?                            → Self-signed via Python cryptography library
```

Self-signed generation is pure Python — no openssl binary needed:

```python
def generate_self_signed(cert_path, key_path, hostnames=None, days_valid=3650):
    from cryptography import x509
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import rsa

    private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    # Build SAN with DNS names + loopback IPs
    san_entries = [x509.DNSName(h) for h in hostnames]
    san_entries.append(x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")))
    san_entries.append(x509.IPAddress(ipaddress.IPv6Address("::1")))
    # ... build and sign cert ...
    key_path.touch(mode=0o600, exist_ok=True)   # restrictive perms BEFORE write
    key_path.write_bytes(key_pem)
    key_path.chmod(0o600)
```

### 5. Constant-time comparison for secrets

Secrets and API tokens should use `hmac.compare_digest` to prevent timing attacks:

```python
if hmac.compare_digest(token, expected_key):
    return await call_next(request)
```

## Template / Starter Code

```python
# auth.py — localhost bypass + auto-generated token
import secrets
from pathlib import Path
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse

TOKEN_PATH = Path.home() / ".config" / "my-tool" / "token"
_LOCALHOST = {"127.0.0.1", "::1"}
_PUBLIC_PATHS = frozenset({"/health", "/auth/status"})

def ensure_token() -> str:
    if TOKEN_PATH.exists():
        token = TOKEN_PATH.read_text().strip()
        if token:
            return token
    token = secrets.token_urlsafe(32)
    TOKEN_PATH.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
    TOKEN_PATH.write_text(token + "\n")
    TOKEN_PATH.chmod(0o600)
    return token

class AuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        # Localhost bypass — socket-level IP, unforgeable
        client_host = request.client.host if request.client else ""
        if client_host in _LOCALHOST:
            return await call_next(request)
        # Public paths
        if request.url.path in _PUBLIC_PATHS:
            return await call_next(request)
        # Bearer token check
        auth = request.headers.get("authorization", "")
        if auth.startswith("Bearer ") and auth[7:] == request.app.state.auth_token:
            return await call_next(request)
        return JSONResponse(status_code=401, content={"detail": "Unauthorized"})
```

> **Note:** Without initializing `app.state.auth_token` and `app.state.auth_required`, the middleware will raise `AttributeError: 'State' object has no attribute 'auth_token'` on the first non-localhost request. Add to your FastAPI lifespan or startup:

```python
# In your FastAPI lifespan or startup:
app.state.auth_token = ensure_token()
app.state.auth_required = resolved_host != "127.0.0.1" and not no_auth
```

## Gotchas & Lessons Learned

1. **WebSocket connections bypass HTTP middleware.** FastAPI's `BaseHTTPMiddleware` is only invoked for HTTP requests, not WebSocket upgrades. Implement a separate `_ws_auth_check()` function that verifies session cookies and bearer tokens on WebSocket connections.

2. **Secrets read fresh from disk per request.** Reading secrets from disk on every request rather than caching at startup lets you rotate them without restarting the server. The alternative (caching at startup) means a rotated secret doesn't take effect until restart.

3. **Self-signed certs need SAN entries, not just CN.** Modern browsers require Subject Alternative Name entries. Include DNS names AND IP addresses in the SAN extension. Without SANs, Chrome rejects the cert even if the CN matches.

4. **Key file permissions: `touch(0o600)` then `write_bytes()` then `chmod(0o600)`.** The `touch` creates the file with restrictive permissions before any content is written. The final `chmod` ensures permissions survive regardless of umask. This belt-and-suspenders approach prevents the window where the key file exists with world-readable permissions.

5. **The redaction-wipe bug.** When `GET /api/settings` returns secret keys as `""` for security, a naive `PATCH` back to the server overwrites real keys with empty strings. The fix preserves existing keys by identifier match with a positional fallback. Any API that redacts secrets in GET responses must handle this in the PATCH/PUT path.
Colle ce Markdown dans ton agent ou utilise les boutons ci-dessus pour l’écrire dans ton projet.

Ce que fait Auth Tls Patterns (Microsoft)

Applique les pratiques du skill auth-tls-patterns__microsoft-amplifier-bundle-skills__skills-db43b7bff964.

Comment utiliser Auth Tls Patterns (Microsoft)

1
Copie le prompt
Un clic copie le prompt packagé (ou l'envoie à ton agent).
2
L'agent installe le skill
Il ajoute le SKILL.md et ses ressources à ton projet.
3
Activation automatique
Le skill s'active dès que le contexte correspond.

Déclencheurs pour Auth Tls Patterns (Microsoft)

Dis simplement à ton agent quelque chose comme :

Applique le skill auth-tls-patterns__microsoft-amplifier-bundle-skills__skills-db43b7bff964 à cette tâche
Utilise auth-tls-patterns__microsoft-amplifier-bundle-skills__skills-db43b7bff964 pour améliorer cette implémentation
Passe en revue ce sujet avec auth-tls-patterns__microsoft-amplifier-bundle-skills__skills-db43b7bff964

Skills liés à Auth Tls Patterns (Microsoft)