Architecture

1. One chain, not four

UniAuthAutoConfiguration#uniAuthSecurityFilterChain installs form login, OAuth2 login and SAML login onto a single SecurityFilterChain.

This is not a simplification waiting to be undone. Chains match in order and the first match wins, so one chain per mechanism would mean only the first ever runs. Every mechanism has to be installed on the same chain to be reachable at all.

2. Conditionals

Every bean is @ConditionalOnMissingBean, so an application overrides by declaring its own rather than by excluding the auto-configuration. uniauth.enabled=false backs off entirely.

The catch-all chain backs off on its own bean name — uniAuthSecurityFilterChain — not on the type. Declaring another SecurityFilterChain therefore no longer costs you every mechanism.

3. Four extension points, before you replace the chain

Declaring your own SecurityFilterChain makes the whole starter back off, so these exist to avoid that. Each was added because something needed it, never speculatively.

uniauth.public-paths

Open up routes without touching the chain.

An AuthenticationEntryPoint bean

The starter uses it instead of redirecting to the chooser. This is how an API-first application answers 401 rather than 302.

A UniAuthAuthorizationCustomizer bean

Contributes authorization rules to this chain, applied in Ordered order after the permitted paths and before the catch-all. This is the one to reach for when the application’s rules are a different shape from "permitted or authenticated" — a role on an admin area, a method-scoped rule, or anything keyed on something other than a path string:

@Bean
UniAuthAuthorizationCustomizer adminRules() {
    return (rules) -> rules
        .requestMatchers("/dev", "/review.html").hasRole("ADMIN")
        .requestMatchers(HttpMethod.POST, "/api/feedback").authenticated()
        .requestMatchers((request) -> request.getLocalPort() == 9090).permitAll();
}

Order is the design: the first matching rule wins, so a rule contributed after anyRequest() would never be reached.

A second SecurityFilterChain

Order it ahead of the starter’s and give it a securityMatcher. Still available, but a customizer is usually better — a second chain separates an application’s rules from the mechanisms that satisfy them, and forces per-chain settings like CSRF and cache-control to be repeated, where any drift between the copies is a bug nobody notices.

4. Provider adapters

Provider quirks ship as opt-in adapters, never as new mechanism types:

  • uniauth.oauth2.github.fetch-email → GithubEmailOAuth2UserService

  • uniauth.oauth2.microsoft.multi-tenant → MicrosoftMultiTenantIdTokenValidator

Both are declared as the beans Spring Security already looks for, so they compose with the single chain instead of needing their own.

5. What is deliberately not here

Registration properties under uniauth. for OAuth2 and SAML.* Those are a large, already-documented configuration surface; forking it would mean maintaining a parallel vocabulary that drifts from Boot’s. UniAuth exposes only an enabled flag for each and consumes the repositories Boot’s own binding produces.

A user store. The internal store exists so a first run is possible and so break-glass accounts have somewhere to live. Anything more is a directory or a database, and both are somebody else’s job.

An admin API. An authentication library has no business exposing a write endpoint. See Examples for how administration is done instead.