# Authelia configuration for the BookStack MCP server. # # Merge these blocks into your existing configuration.yml. Tested against # Authelia 4.39+, where ID Token claims changed (see the claims_policies note). # # Generate the client id and secret first: # # docker run --rm authelia/authelia:latest \ # authelia crypto rand --length 72 --charset rfc3986 # # docker run --rm authelia/authelia:latest \ # authelia crypto hash generate pbkdf2 --variant sha512 \ # --random --random.length 72 --random.charset rfc3986 # # The second command prints BOTH a "Random Password" and a "Digest". # Digest -> client_secret below # Random Password -> OIDC_CLIENT_SECRET in /etc/bookstack-mcp/env # Do not put the digest in the MCP server's env file. identity_providers: oidc: # ----------------------------------------------------------------------- # 1. Claims policy # # Authelia 4.39 stopped putting non-standard claims in the ID Token by # default. The MCP server reads identity from the ID Token (because # Authelia issues opaque access tokens), so groups and preferred_username # have to be put back explicitly. Without this, MCP_REQUIRED_GROUPS will # never match and every tool call gets denied. # ----------------------------------------------------------------------- claims_policies: mcp_claims: id_token: - 'groups' - 'preferred_username' - 'email' - 'email_verified' - 'name' # ----------------------------------------------------------------------- # 2. Lifespan # # Authelia's default refresh_token lifespan is 90m. A claude.ai connector # that sits idle longer than that has to be re-authorised by hand, which # gets old fast. A long refresh token with a short access token is the # right shape for a background integration. # ----------------------------------------------------------------------- lifespans: custom: mcp: access_token: '1h' id_token: '1h' refresh_token: '30d' clients: - client_id: 'REPLACE_WITH_GENERATED_ID' client_name: 'BookStack MCP' client_secret: 'REPLACE_WITH_PBKDF2_DIGEST' public: false # one_factor if you don't want to be prompted for 2FA on reconnect. authorization_policy: 'two_factor' claims_policy: 'mcp_claims' lifespan: 'mcp' # Claude re-authorises on token refresh. Explicit consent every time is # painful; pre-configured remembers the grant for the given duration. consent_mode: 'pre-configured' pre_configured_consent_duration: '1 month' require_pkce: true pkce_challenge_method: 'S256' redirect_uris: - 'https://mcp.example.com/auth/callback' # offline_access is what gets you a refresh token. Without it the # connector dies when the access token expires. scopes: - 'openid' - 'profile' - 'email' - 'groups' - 'offline_access' grant_types: - 'authorization_code' - 'refresh_token' response_types: - 'code' # Authelia's default and what the spec requires. Leave it alone unless # you see 'invalid_client' at the token endpoint, in which case try # 'client_secret_post'. token_endpoint_auth_method: 'client_secret_basic' # Both are already the default; stated explicitly because they matter. # 'none' means access tokens stay opaque rather than being JWTs, which # is exactly why the MCP server needs OIDC_VERIFY_ID_TOKEN=true. access_token_signed_response_alg: 'none' userinfo_signed_response_alg: 'none' # --------------------------------------------------------------------------- # 3. Access control bypass <-- the one everybody misses # # If mcp.example.com is behind Authelia's forward-auth on your reverse proxy, # Claude will be served an HTML login page instead of the MCP endpoint and the # connector will fail with a parse error or a redirect loop. Claude cannot do # cookie-based forward auth; the MCP server runs its own OAuth flow against # Authelia instead. So the host must bypass forward-auth entirely. # # This is not a security hole: the MCP server refuses every unauthenticated # request itself, and Authelia is still the thing deciding who gets a token. # --------------------------------------------------------------------------- access_control: rules: - domain: 'mcp.example.com' policy: 'bypass'