Skip to main content

Authentication

News Dashboard has one primary credential — a signed session cookie — plus two narrow token types for integrations that cannot hold cookies.

Successful login sets an nd_session cookie containing a signed token that carries the user ID and admin flag. The token is signed, not encrypted: it is tamper-evident, and the server re-reads the user record on every request rather than trusting the claims inside it.

Sessions last SESSION_DAYS days (default 30). Expiry is enforced against the token's own age at verification time, so shortening SESSION_DAYS immediately invalidates older cookies.

RouteMethodPurpose
/api/auth/configGETWhich login methods this instance offers.
/api/auth/metadataGETIdentity-provider metadata for the client.
/api/auth/loginPOSTUsername/password login; sets the session cookie.
/api/auth/logoutGETClears the session cookie.
/api/auth/meGETThe current user. Requires a session.

Call /api/auth/config before rendering a login screen — it reports whether this instance uses local passwords, SSO, or both, so clients do not hardcode an assumption.

Email one-time codes

Passwordless login by emailed code, in two steps:

RouteMethodPurpose
/api/auth/otp/requestPOSTSends a one-time code to an email address.
/api/auth/otp/loginPOSTExchanges email + code for a session cookie.

Both steps are throttled per email address and answer 429 with "Too many code requests; try again later" or "Too many code attempts; try again later" when tripped. A successful login clears the failure counter.

An invalid or expired code returns 401 "Invalid or expired code". The request step does not reveal whether an address is registered.

Keycloak SSO

When KEYCLOAK_AUTH_ENABLED is set, browser-redirect SSO routes are mounted alongside the local flows:

RoutePurpose
/auth/loginRedirect to the identity provider.
/auth/registerRedirect to provider-side registration.
/auth/callbackOIDC callback; establishes the session.
/auth/logoutProvider-side logout.

Admin rights can be granted by provider username via KEYCLOAK_ADMIN_USERNAMES. Configuration is covered in Configuration → Authentication.

Guest accounts

A guest session authenticates normally and can read everything, but middleware rejects every mutating request before it reaches a handler:

{ "detail": "Guest accounts cannot modify data" }

This is enforced centrally rather than per-endpoint, so it applies uniformly to routes added later. Clients backed by a guest session should render the UI read-only rather than relying on failed writes.

CSRF protection

Because the session lives in a cookie, cookie-authenticated mutations pass an origin check. If a request carries the session cookie and presents an Origin outside the allowed set, it is rejected:

{ "detail": "Cross-origin request rejected" }

The allowed set derives from the instance's configured CORS origins. Two practical consequences:

  • Browser clients on a different domain need that domain in the CORS configuration, not just permissive CORS headers.
  • Server-to-server integrations should use a token instead of a cookie. Token-authenticated requests do not carry the session cookie and so are not subject to the origin guard.

Integration tokens

Two token families exist for clients that cannot hold a browser cookie. Both are per-user, individually revocable, and scoped to a single integration.

MCP tokens

RouteMethodPurpose
/api/users/me/mcp-tokensGETList your MCP tokens.
/api/users/me/mcp-tokensPOSTMint a token.
/api/users/me/mcp-tokens/{token_id}DELETERevoke a token.

These authenticate the read-only MCP tool set. MCP bearer authentication is independent of Keycloak and browser sessions. The server is enabled by default; setting MCP_SERVER_ENABLED to false, 0, no, or off disables the MCP transport and blocks creation of new tokens. Existing tokens remain stored so access can resume if the feature is enabled again, and revocation invalidates a token immediately.

Scopes are enforced per tool: search grants MCP listing, source discovery, and search; read grants MCP single-article retrieval; ask grants MCP ask_news; and briefings grants complete saved-briefing list and detail. The same ask scope can authorize the optional A2A question-answering endpoint when A2A is separately enabled. The scope is shared, but the routes and feature flags are independent: MCP uses MCP_SERVER_ENABLED, while A2A remains disabled unless A2A_SERVER_ENABLED=true. Enabling one does not enable the other. Prefer separate least-privilege tokens for clients that do not need both surfaces.

The plaintext ndmcp_ credential is returned only when it is minted; the database stores its SHA-256 hash and display prefix. Rotate a credential by creating a replacement with the minimum scopes, updating and verifying the client, then revoking the old token. Revocation takes effect immediately. Neither the token nor the MCP protocol carries direct Keycloak credentials.

Google Reader tokens

RouteMethodPurpose
/api/users/me/greader-tokensGETList sync tokens.
/api/users/me/greader-tokensPOSTMint a token.
/api/users/me/greader-tokens/{token_id}DELETERevoke a token.

These back the Google Reader-compatible sync API used by third-party feed readers.

Podcast feed tokens

Podcast feeds are consumed by player apps that cannot log in, so the feed URL carries its own capability token:

RouteMethodPurpose
/api/briefings/podcast-feed-tokenGETFetch the current feed token.
/api/briefings/podcast-feed-token/regeneratePOSTRotate it.

The token grants read access to that user's generated podcast audio and nothing else. Regenerating invalidates the previous URL — that is the only way to revoke a feed that has been shared.

Account lifecycle

RouteMethodPurpose
/api/users/me/exportGETExport your data.
/api/users/me/importPOSTImport a previously exported archive.
/api/users/meDELETEDelete your account and associated data.

Import accepts JSON archives up to 20 MiB. Administrative user management is separate and documented under Operations.