microsoft/auth-tls-patterns
> 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.
npx skills add https://github.com/microsoft/amplifier-bundle-skills --skill auth-tls-patterns
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.
The single most important auth decision: localhost connections skip all auth checks. But you MUST use the socket-level client IP, not HTTP headers:
_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:
auth_required = resolved_host != "127.0.0.1" and not no_auth
Resolve auth mode through a fallback chain:
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:
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
A simpler bearer token approach:
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:
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"})
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:
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)
Secrets and API tokens should use hmac.compare_digest to prevent timing attacks:
if hmac.compare_digest(token, expected_key):
return await call_next(request)
# 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:
# 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
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.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.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.Take microsoft/auth-tls-patterns from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.