
Every request comes from a requester: a SAML Service Provider known by its entityID, or an OpenID Connect client
known by its client_id. Before the server can process a request it needs to know who the requester is. That is what
the client registry is for.
The registry is one interface for both protocols. Code that does not care about the protocol, such as the login page or a check that the requester holds a certain mark, works against the protocol-neutral part of what the registry answers. The SAML and OpenID Connect layers reach for the protocol metadata inside it.
Source links in this guide point to the main branch of the
spring-authentication-server repository.
ClientRegistry has one lookup, by protocol and requester identity:
RequesterRecord record = clientRegistry.lookup(AuthenticationProtocol.OIDC, clientId);
if (record == null) {
// The registry does not know this client.
}
A requester that the registry does not know gives null. That is a normal outcome, and it is not the same thing as a
backend that fails. A metadata source that cannot be read, or a resolver service that cannot be reached, throws a
ClientRegistryException. A failure is never reported as an unknown requester, so a
temporary problem behind the registry never turns into “no such client”.
A RequesterRecord holds a protocol-neutral part and the protocol metadata.
The protocol-neutral part is:
Requester, that is, the protocol and the requester identity.Picking a name or a logotype for the user’s language is done by the record:
String name = record.getDisplayName("sv");
Logo logo = record.getLogo("sv");
The name for the exact language tag is used if there is one, otherwise a name for a language with the same primary
subtag, so that sv-SE in the metadata answers a request for sv, and otherwise the name that is not tied to a
language.
The protocol metadata is carried as it is and the core never looks at it. Ask for it in its own type:
EntityDescriptor spMetadata = record.getProtocolMetadata(EntityDescriptor.class);
OIDCClientMetadata clientMetadata = record.getProtocolMetadata(OIDCClientMetadata.class);
Marks are one set of identifiers for both protocols:
Consumers see identifiers and nothing else. That a trust mark is valid is the backend’s business, checked before the
type is put in the set, so record.hasMark(mark) means that the requester holds the mark:
if (!record.hasMark("http://id.elegnamnden.se/sprop/1.0/mobile-auth")) {
...
}
A mark that the operator has set on a configured client, or on a client held in a repository, is there because the operator vouches for it. Those are not verified.
record.organizationNumber() tells which organisation the requester belongs to, so that code such as login page
support, audit and checks for accepting a requester does not have to read SAML or OpenID Connect metadata:
mdorgext:OrganizationNumber extension of the Service Provider’s md:Organization, see the
schema.organization_identifier in the client metadata, see
OpenID Federation Organization Identifier Metadata Parameter 1.0
and Section 3 of
Sweden Connect - OpenID Connect Metadata Requirements.The value is held exactly as the metadata publishes it, and it is not validated. The two protocols use different
forms. A SAML value may be 556677-8899, 5566778899 or anything else, since the schema does not restrict it, while
an OpenID Connect value is a GLUE URI such as urn:glue:iso6523:0007:5566778899. Code that compares requesters across
protocols handles both forms itself.
The organisation number is optional. A requester whose metadata gives none has null in its record.
The registry itself holds no data. It asks its backends, and each backend answers for one protocol. Several backends
may serve the same protocol, in which case they are asked in the order they were given and the first one that knows
the requester wins. ClientRegistryBackend is the interface, and
DefaultClientRegistry is the registry that asks them.
Service Provider metadata is read through one or more MetadataSource objects, and
MetadataProviderFactory turns them into the OpenSAML provider that
SamlMetadataBackend serves. The location of a source decides how it is read:
| Location | How it is read |
|---|---|
| An HTTP or HTTPS URL | Downloaded as a federation metadata document |
An HTTP or HTTPS URL with mdq set |
Queried entity by entity with the MDQ protocol |
| A file | Read from the file system and reloaded when it changes |
| Anything else, such as a classpath resource | Read once and held in memory |
MetadataSource source = MetadataSource.builder(new UrlResource("https://md.swedenconnect.se/role/sp.xml"))
.validationCertificate(certificate)
.backupLocation(new File("/var/authn-server/metadata-backup.xml"))
.build();
SamlMetadataBackend backend =
new SamlMetadataBackend(MetadataProviderFactory.createMetadataResolver(List.of(source), sslBundles));
Give several sources and they are combined into one, searched in the order they were given. That is how a federation metadata source and a locally kept Service Provider can live side by side.
For a downloading source:
The display names of a record are taken from the mdui:UIInfo extension, then from
Organization/OrganizationDisplayName and finally from Organization/OrganizationName. For every language the first
name found is the one used. Logotypes come from mdui:UIInfo. The marks are the entity categories that the Service
Provider declares. The organisation number comes from the mdorgext:OrganizationNumber extension of
md:Organization; a Service Provider without md:Organization, or without the extension, has none.
The SAML configurer creates this backend from the metadata sources it is given, and with Spring Boot the sources are
set with the authn-server.saml.metadata-providers properties, see Configuration.
The OpenID Connect side has three backends. They are asked in a configured order, and the default order is configuration, repository, federation.
ConfigurationClientBackend): the clients that the deployment
gives the server directly. With Spring Boot, they are given with authn-server.oidc.clients, in JSON files or
inline, see Clients, and that source is named properties. Configured clients
added in code get the name configuration. The clients are read with
OidcClientReader, which may also be used without Spring Boot.RepositoryClientBackend): the clients of a
ClientRepository. The library ships InMemoryClientRepository;
implementations backed by a database are added later. A repository that can list its clients implements
findAll(), so that its clients can be listed and compared with those known before a restart, see
Monitoring and managing the server.FederationClientBackend): clients resolved through OpenID Federation.Every client source has a name, which the Actuator and the audit events use to tell
the sources apart. The names must be unique among the client sources of a protocol, and the application does not
start if two sources of the same protocol have the same name. The properties source is asked before any OpenID
Connect client source added in code.
The first two work from an OidcClientRecord, which is a client_id, the client metadata as
Nimbus models it, and the trust mark types that the operator has set:
OIDCClientMetadata metadata = new OIDCClientMetadata();
metadata.setName("Tjänsten", LangTag.parse("sv"));
metadata.setName("The Service", LangTag.parse("en"));
metadata.setLogoURI(URI.create("https://client.example.com/logo.svg"));
ClientRegistryBackend backend = new ConfigurationClientBackend(
List.of(new OidcClientRecord("https://client.example.com", metadata, Set.of(trustMarkType))));
The display names of a record come from client_name and the logotypes from logo_uri, both in every language that
the metadata gives them in. The Sweden Connect profile requires client_name in Swedish and English, so a client that
follows the profile gives the login page a name in both languages. The organisation number comes from
organization_identifier in the client metadata, for all three backends. A federation client gets it from the
resolved metadata, where the Sweden Connect registration intermediate sets it. For a configured client, or a client in
a repository, the operator puts it in the client metadata:
metadata.setCustomField("organization_identifier", "urn:glue:iso6523:0007:5566778899");
A configured client, or a client in a repository, that authenticates with a client secret at the token endpoint has
the secret in the client_secret field of its metadata, see
Client authentication.
The federation backend resolves a client through OpenID Federation, where the client_id of the client is its entity
identifier.
With Spring Boot, it is set up from properties: the trust anchor under authn-server.oidc.federation.trust-anchor and
the source under authn-server.oidc.federation.clients, with the resolver, the trust mark issuers and the cache, see
Clients from OpenID Federation. The source is named federation, and it
is on whenever the OpenID Provider is a member of a federation, unless it is turned off. Endpoints that are not
configured are found in the entity configurations of the resolver and the issuers, by
EntityConfigurationEndpoints.
The OpenID Connect sources are asked in this order: the configured clients of authn-server.oidc.clients
(properties), then the sources added in an adapter, and the federation source last. A client known to both the
configured clients and the federation is therefore served from the configured clients.
The rest of this section describes the parts, for an application that replaces some of them, for example with a
resolver of its own. A FederationClientBackend that an adapter adds replaces the one of the properties, which is
then not set up. The backend is put together from a FederationResolver, a
TrustMarkRequester, a FederationCache and
FederationCacheSettings:
FederationSettings federation = new FederationSettings(
new FederationSettings.TrustAnchor(trustAnchorEntityId, trustAnchorKeys),
new FederationSettings.Resolver(resolverEntityId, resolveEndpoint, resolverKeys),
Map.of(trustMarkType, new FederationSettings.TrustMarkIssuer(issuerEntityId, trustMarkEndpoint, issuerKeys,
trustMarkStatusEndpoint)));
FederationClientBackend backend = new FederationClientBackend(
new HttpFederationResolver(federation),
new HttpTrustMarkRequester(federation),
new InMemoryFederationCache(),
FederationCacheSettings.defaults());
HttpFederationResolver calls the resolve endpoint of an external resolver service. The
resolve response is a signed JWT, and it is verified with the trust anchor’s keys when the resolver runs at the trust
anchor, that is, when it has the same entity identifier, and with the resolver’s own keys otherwise. The keys of a
resolver that is not the trust anchor are configured together with it. A resolver given without an endpoint, null,
is called at the federation_resolve_endpoint that it publishes in its entity configuration, and the same holds for
the trust mark endpoint and the status endpoint of an issuer. The record holds the client metadata
of the response, after the metadata policies of the trust chain have been applied by the resolver, and the trust mark
types of the response.
Resolving inside the OpenID Provider itself is a later addition. It arrives as another implementation of
FederationResolver, and nothing else changes.
Resolved clients are cached, because reaching the resolver on every request is not an option.
InMemoryFederationCache, which is what a single node needs.
RedisFederationCache keeps the entries in Redis, shared by all nodes.client_id do not each reach the resolver. A resolver that fails is never
cached.FederationCacheRefresher is a background job that resolves a cached client again
shortly before its entry expires, so that a request from a client that is used often never waits for the resolver.
The job is off by default.
Only clients that are worth it are refreshed. A client qualifies once it has been looked up a configured number of times within a configured period, and one run never refreshes more clients than a configured maximum. Clients that are used rarely are left alone; they are resolved again the next time they are asked for after their entry has expired. The count of lookups is bounded too: at most a configured number of clients are counted, and the client that has not been asked for in the longest time is dropped when a new one arrives.
FederationCacheSettings settings = new FederationCacheSettings(
Duration.ofHours(12),
Duration.ofMinutes(1),
FederationCacheSettings.RefreshSettings.enabled(true));
FederationCacheRefresher refresher = new FederationCacheRefresher(backend);
refresher.start();
Built from the backend, the refresher uses the cache, the resolver and the settings of the backend, and a client that the resolver no longer knows is reported as removed. The constructor that takes the parts one by one is still there, but a refresher built that way reports nothing.
A resolve response carries the trust marks that the resolver knows about. A trust mark that it does not carry can be asked for from its issuer:
RequesterRecord record = clientRegistry.requestMark(requester, trustMarkType);
if (record != null && record.hasMark(trustMarkType)) {
...
}
exp, or with a long one, stays trusted after the issuer has withdrawn it, unless its status is
checked, see Checking the status of trust marks.For SAML, asking for a mark never finds anything. It gives the requester’s record unchanged.
TrustMarkStatusChecker is a background job that asks the issuer whether the trust marks
obtained on demand are still valid, at the issuer’s trust mark status endpoint (OpenID Federation 1.0, Section 8.4).
It runs at a configurable interval, one hour by default:
TrustMarkStatusChecker checker = new TrustMarkStatusChecker(cache, federation, httpFederationClient,
Duration.ofHours(1), Clock.systemUTC());
checker.setEventPublisher(applicationContext);
checker.start();
With Spring Boot, httpFederationClient is the HttpFederationClient bean, so that the
status checks count toward the health of the trust mark issuers.
exp, or when its exp is later than the next check. A trust mark that
expires before the next check is not checked; it expires as before.statusEndpoint of
FederationSettings.TrustMarkIssuer. A checker built with an
EntityConfigurationEndpoints checks an issuer without a configured status endpoint
at the federation_trust_mark_status_endpoint that it publishes, as the one set up from the properties does. The
trust marks of an issuer without a status endpoint are not checked.active, such as revoked or expired, is removed from the
client’s cached record. A client that needs it for the required marks is then treated as not
holding it on its next request: the trust mark is asked for again, and when the issuer no longer issues it, the
client is rejected with the same error response as any client that lacks a required mark. The removal is also
published as a system alert for the audit log, when the checker has been given an event publisher, see
Auditing.WARN, and the trust mark is checked again at the next run.The trust marks of the resolve response are not checked by the job. The resolver has checked them, and they are held no longer than the resolve response is valid.
The cache is also where the background jobs keep their shared state. The backend asks the cache for the object that
counts lookups, and the jobs ask it for the lock that decides which node runs a round. With a
RedisFederationCache:
With Spring Boot, a FederationCache bean is declared according to authn-server.storage.type, or
authn-server.oidc.storage.federation-cache, see Running several nodes.
The source set up from the properties uses it, together with the jobs. An application that builds the backend in code
builds it and the jobs from the same bean, and from the HttpFederationClient bean that the
auto-configuration declares. That client publishes the outcome of every call it makes, which is what the
health of the federation services is built from:
@Bean
FederationClientBackend federationBackend(final FederationCache cache, final HttpFederationClient client) {
return new FederationClientBackend(new HttpFederationResolver(federation, client),
new HttpTrustMarkRequester(federation, client), cache, settings);
}
The backend is added to the client registry with an AuthnServerConfigurerAdapter, which calls
configurer.clientRegistryBackend(backend). The clients that the backend adds and removes are then reported, see
Clients added and removed, and the clients it holds can be listed and
updated through the clients endpoint.
Knowing a requester is not the same as accepting it. Once the requester has been found in the registry and its
request has been verified, a RequesterAcceptance check decides whether it may use the server.
A requester that is not accepted gets the error NOT_AUTHORIZED, answered to the requester as an error response. For
SAML that is the status Responder / RequestDenied, and for OpenID Connect the error unauthorized_client.
The check sees the requester’s record: the protocol-neutral part and the protocol metadata. Two implementations are supplied:
RequesterAcceptance.acceptAll(). It is the default when nothing is configured.ConfigurableRequesterAcceptance, which holds a set of
RequesterPredicates.Every predicate belongs to one protocol, and only the predicates of the requester’s protocol are evaluated, so a rule
for SAML Service Providers never affects an OpenID Connect client. Per protocol, the predicates are combined in one of
two modes: ALL, the default, where every predicate must accept the requester, or ANY, where one accepting predicate
is enough. A protocol with no predicates accepts every requester.
Two predicates are built in:
WhitelistRequesterPredicate accepts the requesters of a list of identities: SAML
entityIDs or OpenID Connect client_ids.RequiredMarksRequesterPredicate requires marks, given as groups. Every group must
be satisfied, and a group is satisfied by any one of its marks, so [[A, B], [C]] means “A or B, and C”. When a
group is not satisfied by the record, the predicate asks the registry for the missing marks, see
Trust marks on demand, and evaluates the updated record before rejecting. For SAML nothing
is obtained this way, so the Service Provider metadata decides.With Spring Boot, the configurable check is set up from properties under each protocol’s prefix, for SAML
authn-server.saml.requester-acceptance.*, see Configuration, and for
OpenID Connect authn-server.oidc.requester-acceptance.*, see
Configuration.
A predicate of your own is added with an adapter. A predicate that reads protocol metadata gets it from the record in its own type:
@Bean
AuthnServerConfigurerAdapter onlySwedishOrganisations() {
return (http, configurer) -> configurer.configurableRequesterAcceptance().addPredicate(new RequesterPredicate() {
@Override
public AuthenticationProtocol getProtocol() {
return AuthenticationProtocol.SAML;
}
@Override
public boolean test(final RequesterRecord record, final ClientRegistry registry) {
return record.organizationNumber() != null;
}
});
}
To replace the check altogether, assign another implementation with configurer.requesterAcceptance(...).
Copyright © 2026, Sweden Connect. Licensed under version 2.0 of the Apache License.