Skip to content

feat(security, authc): implement optional "minimal" authentication mode#251119

Merged
azasypkin merged 8 commits intoelastic:mainfrom
azasypkin:issue-xxx-minimal-authc-opt-in
Mar 20, 2026
Merged

feat(security, authc): implement optional "minimal" authentication mode#251119
azasypkin merged 8 commits intoelastic:mainfrom
azasypkin:issue-xxx-minimal-authc-opt-in

Conversation

@azasypkin
Copy link
Copy Markdown
Contributor

@azasypkin azasypkin commented Jan 30, 2026

Summary

This PR introduces a "minimal" authentication mode for Kibana HTTP routes and updates the authentication provider interfaces to pass full session objects instead of raw provider state.

Minimal authentication mode

Adds a new 'minimal' option for security.authc.enabled on route definitions. When a route opts into minimal authentication, Kibana skips the Elasticsearch _authenticate API call and instead returns a lightweight user proxy constructed from session data already stored in the Kibana session document.

This is useful for high-frequency or performance-sensitive endpoints where:

  • The session has already been fully authenticated on login
  • Credential validation will happen naturally when the request reaches Elasticsearch
  • The overhead of an extra _authenticate round-trip is unnecessary

The minimal user proxy provides username, authentication_provider, profile_uid, and enabled, but deliberately throws if code tries to access properties that require a real ES authenticate call (authentication_realm, lookup_realm, authentication_type, elastic_cloud_user).

Route type system changes

The RouteAuthc type is expanded from AuthcEnabled | AuthcDisabled to AuthcEnabled | AuthcMinimal | AuthcOptional | AuthcDisabled:

  • AuthcEnabled (enabled: true) - full authentication (default, unchanged)
  • AuthcMinimal (enabled: 'minimal') - new, session-only authentication, no ES call, requires reason
  • AuthcOptional (enabled: 'optional') - existing behavior, now requires an explicit reason
  • AuthcDisabled (enabled: false) - no authentication (unchanged)

Both 'minimal' and 'optional' now require a reason string explaining why the route deviates from the default. Existing 'optional' routes are migrated to the new shape with explicit reasons.

Provider interface refactoring

All authentication provider methods (login, authenticate, logout) now receive the full SessionValue<TState> object instead of raw state:

  • BaseAuthenticationProvider becomes generic: BaseAuthenticationProvider<TState>
  • logout(request, state?)logout(request, session?)
  • Providers extract state from session?.state internally when needed
  • The Authenticator passes sessionValue (not sessionValue.state) to providers
  • This gives providers access to session metadata (username, provider, userProfileId) needed for minimal auth

Route migrations

Existing routes using options.authRequired: 'optional' are migrated to the new security.authc config:

  • GET /api/status - status endpoint (k8s probes)
  • GET /api/banners/info - banner info on login page
  • GET /api/custom_branding/info - custom branding on login page
  • GET /bootstrap-anonymous.js - anonymous bootstrap script
  • POST /internal/security/analytics/_record_violations - CSP violation reports
  • POST /login - login page view route (reason added)
  • Mock IDP plugin routes (testing)

Integration tests

Every authentication provider (anonymous, basic/token, SAML, OIDC, PKI, Kerberos) gets a 'should support minimal authentication' integration test that:

  1. Authenticates and obtains a session cookie via the provider's normal flow
  2. Hits both /authentication/fast/me (minimal) and /internal/security/me (default)
  3. Asserts that username and authentication_provider match between both responses
  4. Asserts the key behavioral difference: minimal mode does not return authentication_realm (since ES _authenticate is skipped), while default mode does

Assisted by: Claude Opus 4.6 (via OpenCode and GitHub Copilot).

Closes #244928

@azasypkin azasypkin force-pushed the issue-xxx-minimal-authc-opt-in branch from 5ecbc43 to 4d76173 Compare March 9, 2026 19:13
@azasypkin azasypkin self-assigned this Mar 9, 2026
@azasypkin azasypkin added release_note:skip Skip the PR/issue when compiling release notes backport:skip This PR does not require backporting labels Mar 9, 2026
@azasypkin azasypkin marked this pull request as ready for review March 10, 2026 16:43
@azasypkin azasypkin requested review from a team as code owners March 10, 2026 16:43
*/
export const ELASTIC_CLOUD_SSO_REALM_NAME = 'cloud-saml-kibana';

/**
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: this is the core change of the entire PR.

username: session.username,
authentication_provider: session.provider,
profile_uid: session.userProfileId,
// TODO: Currently audit logs rely on `roles` property being present on the user object.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also expand the list of properties we store in the session document to have everything we need.

return AuthenticationResult.notHandled();
}

const authHeaders = {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm moving this out of try\catch here and in other providers as we don't expect this to fail.

Copy link
Copy Markdown
Contributor

@rgodfrey-elastic rgodfrey-elastic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving with a pair on optional follow ups

Comment thread src/core/packages/http/server/src/router/route.ts
get: (target, prop, receiver) => {
const value = Reflect.get(target, prop, receiver);
if (USER_PROPERTIES_NOT_AVAILABLE_IN_MIN_AUTHC_MODE.has(prop.toString())) {
throw new Error(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I missed it I don't see any tests tests for this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, let me double check and add more tests if it's the case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 015b576

Copy link
Copy Markdown
Contributor

@TinaHeiligers TinaHeiligers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM from Core's side.

{
path: '/bootstrap-anonymous.js',
security: {
authc: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explicitly specifying auth mode with a clear reason is a Huge improvement over the someone obscure optional auth, nice!

enabled: schema.oneOf([
schema.literal(true),
schema.literal('optional'),
schema.literal('minimal'),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first glance, plugin owners might adopt minimal auth to avoid round-tripping to ES. We'll need to make sure use remains appropriate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Eventually, however, we'd like to enable minimal authentication for all routes by default, as the absolute majority of routes either don't need any user information or need only what's already available in the Kibana session. For the remaining routes, we might expose dedicated asynchronous programmatic APIs to fetch that additional information (e.g., a list of roles).

@elasticmachine
Copy link
Copy Markdown
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Test Failures

  • [job] [logs] Scout: [ observability / profiling ] plugin / local-stateful-classic - APM integration not installed but setup completed - Admin user

Metrics [docs]

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
@kbn/core-http-server 248 252 +4

Public APIs missing exports

Total count of every type that is part of your API that should be exported but is not. This will cause broken links in the API documentation system. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats exports for more detailed information.

id before after diff
@kbn/core 990 992 +2
Unknown metric groups

API count

id before after diff
@kbn/core-http-server 581 587 +6

References to deprecated APIs

id before after diff
@kbn/core 424 422 -2
banners 1 0 -1
customBranding 1 0 -1
mockIdpPlugin 6 4 -2
security 56 55 -1
telemetry 5 4 -1
total -8

Unreferenced deprecated APIs

id before after diff
@kbn/core 424 422 -2
banners 1 0 -1
customBranding 1 0 -1
mockIdpPlugin 6 4 -2
security 56 55 -1
telemetry 5 4 -1
total -8

cc @azasypkin

@azasypkin
Copy link
Copy Markdown
Contributor Author

We run various performance tests with this change, and here are the results:

First, @drewdaemon applied this change to the /internal/search/{...} route and ran "dashboard performance" journeys. These didn't show any improvement at all. In some cases, we even saw a small regression, but in the end, we believe it's just noise. It's been a while since we ran these tests in the past when we saw more meaningful performance improvements, and many things have changed since then that might have made this improvement less noticeable compared to other parts of request processing, specifically, fetching data from Elasticsearch itself.

This is not super surprising and was somewhat expected since the _authenticate call we eliminate with this change is rather lightweight, although still not free. To see the real impact, I ran more targeted tests with autocannon against the /internal/search/ese endpoint using the e-commerce sample dataset with a very light payload/response to balance things out. I ran these tests multiple times locally (obviously not an ideally stable environment for such tests, and Kibana feels overloaded), and the lowest improvement I saw among these runs was around 4.5%, while the largest was around 18%.

Here is one of the results (~10.2% improvement in mean latency, you can access the interactive version at x.secutils.dev/perf/minimal-authc):

node compare_search_authc_perf.mjs normal_saml_results.json minimal_saml_results.json

╔═══════════════════════════════════════════════════════════╗
║  SEARCH ROUTE AUTHC PERFORMANCE COMPARISON                ║
║                                                           ║
║  normal_saml (baseline)  vs  minimal_saml                 ║
║  Rounds: 5 | Connections: 10 | Duration: 60s per round    ║
╚═══════════════════════════════════════════════════════════╝

  Metric                         normal_saml    minimal_saml
  ───────────────────────────────────────────────────────────
  Mean Latency (ms)                37.1 ±0.733.3 (-10.2% ↓✓)
  p50 Latency (ms)                      33.8 30.6 (-9.5% ↓✓)
  p75 Latency (ms)                      37.8 34.4 (-9.0% ↓✓)
  p99 Latency (ms)                      80.8 89.4 (+10.6% ↑)
  Requests/sec                         266.4296.4 (+11.3% ↑✓)
  Throughput (KB/s)                    622.8548.7 (-11.9% ↓)
  ───────────────────────────────────────────────────────────

  Statistical tests (Welch's t-test on per-round mean latency, vs baseline):
    minimal_saml: t=6.696  df=6.6  p=0.0004  significant: ✓ YES (p < 0.05)  |  Cohen's d=4.235 (large)

  VERDICT  minimal_saml: 10.2% FASTER — statistically significant improvement.
image

===

minimal_authc_perf.zip - tools used for capture these results + produced results

===

With that, I believe it's still worth merging and relying on this change in performance-critical code paths. We also need to get real feedback before we consider making minimal authentication the default mode for all HTTP routes in Kibana, and using it for /internal/search/{...} will give us a chance to get that feedback.

@azasypkin azasypkin merged commit 5286e34 into elastic:main Mar 20, 2026
18 checks passed
@azasypkin azasypkin deleted the issue-xxx-minimal-authc-opt-in branch March 20, 2026 15:06
@drewdaemon
Copy link
Copy Markdown
Contributor

++ thank you for this feature. We don't really have to prove that doing less work is faster, but it's great to see the validation in the more-focused tests as well 👏

jeramysoucy pushed a commit to jeramysoucy/kibana that referenced this pull request Mar 26, 2026
…de (elastic#251119)

## Summary

This PR introduces a **"minimal" authentication mode** for Kibana HTTP
routes and updates the authentication provider interfaces to pass full
session objects instead of raw provider state.

### Minimal authentication mode

Adds a new `'minimal'` option for `security.authc.enabled` on route
definitions. When a route opts into minimal authentication, Kibana
**skips the Elasticsearch `_authenticate` API call** and instead returns
a lightweight user proxy constructed from session data already stored in
the Kibana session document.

This is useful for high-frequency or performance-sensitive endpoints
where:
- The session has already been fully authenticated on login
- Credential validation will happen naturally when the request reaches
Elasticsearch
- The overhead of an extra `_authenticate` round-trip is unnecessary

The minimal user proxy provides `username`, `authentication_provider`,
`profile_uid`, and `enabled`, but deliberately **throws** if code tries
to access properties that require a real ES authenticate call
(`authentication_realm`, `lookup_realm`, `authentication_type`,
`elastic_cloud_user`).

### Route type system changes

The `RouteAuthc` type is expanded from `AuthcEnabled | AuthcDisabled` to
`AuthcEnabled | AuthcMinimal | AuthcOptional | AuthcDisabled`:

- **`AuthcEnabled`** (`enabled: true`) - full authentication (default,
unchanged)
- **`AuthcMinimal`** (`enabled: 'minimal'`) - new, session-only
authentication, no ES call, requires `reason`
- **`AuthcOptional`** (`enabled: 'optional'`) - existing behavior, now
requires an explicit `reason`
- **`AuthcDisabled`** (`enabled: false`) - no authentication (unchanged)

Both `'minimal'` and `'optional'` now require a `reason` string
explaining why the route deviates from the default. Existing
`'optional'` routes are migrated to the new shape with explicit reasons.

### Provider interface refactoring

All authentication provider methods (`login`, `authenticate`, `logout`)
now receive the full `SessionValue<TState>` object instead of raw
`state`:

- `BaseAuthenticationProvider` becomes generic:
`BaseAuthenticationProvider<TState>`
- `logout(request, state?)` → `logout(request, session?)`
- Providers extract `state` from `session?.state` internally when needed
- The `Authenticator` passes `sessionValue` (not `sessionValue.state`)
to providers
- This gives providers access to session metadata (`username`,
`provider`, `userProfileId`) needed for minimal auth

### Route migrations

Existing routes using `options.authRequired: 'optional'` are migrated to
the new `security.authc` config:

- `GET /api/status` - status endpoint (k8s probes)
- `GET /api/banners/info` - banner info on login page
- `GET /api/custom_branding/info` - custom branding on login page
- `GET /bootstrap-anonymous.js` - anonymous bootstrap script
- `POST /internal/security/analytics/_record_violations` - CSP violation
reports
- `POST /login` - login page view route (reason added)
- Mock IDP plugin routes (testing)

### Integration tests

Every authentication provider (anonymous, basic/token, SAML, OIDC, PKI,
Kerberos) gets a `'should support minimal authentication'` integration
test that:

1. Authenticates and obtains a session cookie via the provider's normal
flow
2. Hits **both** `/authentication/fast/me` (minimal) and
`/internal/security/me` (default)
3. Asserts that `username` and `authentication_provider` match between
both responses
4. Asserts the key behavioral difference: minimal mode does **not**
return `authentication_realm` (since ES `_authenticate` is skipped),
while default mode does

__Assisted by:__ Claude Opus 4.6 (via OpenCode and GitHub Copilot).

Closes elastic#244928
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting release_note:skip Skip the PR/issue when compiling release notes v9.4.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Minimal AuthC] Introduce opt-in configuration

8 participants