Skip to main content
Version: v0.25.0 (Latest)

AS Container

sphereon/enterprise-as is the OAuth 2.0 / OpenID authorization server that issuer-side flows need to delegate token issuance to. It is deliberately not a general-purpose IAM product. Its scope is the flows OID4VCI and OID4VP actually require, plus the minimum machine-to-machine surface for tenant service clients.

That scope is precise enough to call out:

  • The OID4VCI pre-authorized code grant, end to end. The AS mints pre-authorized codes for the issuer, exchanges them for access tokens, and emits the c_nonce the wallet binds its proof to.
  • The OID4VCI authorization code grant via wallet federation. The AS is the OAuth endpoint the wallet talks to. /authorize delegates the actual user-authentication step to the tenant's configured upstream IdP via the federation flow; the AS owns the protocol mechanics (state, PKCE, code issuance, token exchange, claims projection).
  • Per-tenant OAuth 2.0 client_id/client_secret issuance for backend service clients. Available regardless of whether the tenant has an external AS configured.

User account management, MFA, account self-service, password authentication, and an in-AS user store are deferred. The UserAuthenticationProvider SPI stays open in the EDK so a future release can add an in-AS user store without rewriting the surface, but as shipped, the AS routes user authentication to an upstream IdP through the federation flow rather than authenticating users itself.

AS container endpoints and tenant flows

Public Protocol Surface

Thirteen HTTP adapters together cover the OAuth and OpenID protocol:

  • OAuth2AuthorizationHttpAdapter: /authorize. Handles the wallet-federation handoff and the standard authorization flow.
  • OAuth2TokenHttpAdapter: /token. Code exchange, pre-auth-code exchange, client-credentials exchange, refresh exchange.
  • OAuth2DiscoveryHttpAdapter: /.well-known/oauth-authorization-server (with tenant slug per the well-known URL forms).
  • OAuth2OpenidDiscoveryPathIssuerHttpAdapter: /{tenant-slug}/.well-known/openid-configuration.
  • OAuth2UserInfoHttpAdapter: /userinfo.
  • OAuth2EndSessionHttpAdapter: RP-initiated logout.
  • OAuth2FederationHttpAdapter: the federation handshake with upstream IdPs (the EDK's bridge to OIDC providers the tenant uses for user authentication).
  • OAuth2DeviceAuthorizationHttpAdapter / OAuth2DeviceVerificationHttpAdapter, the device flow.
  • OAuth2LoginHttpAdapter: the consent/login UI shell.
  • OAuth2AttestationHttpAdapter: wallet attestation handling.
  • OAuth2InternalHttpAdapter and AbstractOAuth2HttpAdapter, internal plumbing.

Of these, the device flow, attestation, the login UI, and end-session are at a varying state of completeness across releases. The pre-auth + federation + client-credentials path is the production-ready core.

Per-Tenant Federation Providers

The federation flow is where the AS connects to the tenant's upstream IdP for user authentication. A tenant administrator registers federation providers through the tenant-IdP admin REST (/api/v1/oauth2/federation/providers). Each provider entry carries the discovery URL, client credentials, scopes, and the claim mapping that translates the upstream IdP's userinfo into the claims projected into the AS-issued access token.

The federation REST surface includes:

  • TenantIdpCrudHttpEndpointCommands: registering, listing, updating, and deleting federation providers per tenant.
  • TenantIdpStateHttpEndpointCommands: enabling, disabling, and reading the runtime state of a provider.
  • TenantIdpConnectivityHttpEndpointCommands: a test-connection endpoint that drives the discovery fetch and surfaces network and JWKS errors before the provider is enabled for live traffic.

The runtime federation endpoint commands (FederationAuthorizeHttpEndpointCommandImpl, FederationCallbackHttpEndpointCommandImpl, ReconciliationAuthorizeHttpEndpointCommandImpl, ReconciliationCallbackHttpEndpointCommandImpl) drive the handshake itself. The FederationBaseUrlResolver keeps callback URLs consistent with the tenant's public-endpoint binding.

When a tenant has multiple federation providers configured, the wallet can pick at the IdP-discovery step. When a tenant has none, the federation flow is unavailable for that tenant and only the pre-authorized code and client-credentials flows are usable.

Per-Tenant Client Registry

Clients are tenant-scoped. The AS admin REST (/api/v1/oauth2/clients) covers registering, listing, updating, and deleting client_id/client_secret pairs. Dynamic registration (/oidc/register) is also wired through, with per-tenant policy controlling whether it is allowed.

Client secrets are not stored in plaintext. The AS hashes them on registration and verifies against the hash on /token. Refresh tokens, access tokens, and pre-authorized codes are stored hashed where they need to be persisted at all.

Tenant-Aware URL Generation

Every URL the AS advertises (the issuer value in metadata, the jwks_uri, the authorization_endpoint, the token_endpoint, the userinfo_endpoint, the end_session_endpoint, the federation callback URLs) comes from the tenant's public-endpoint binding rather than the request host. The lib-oauth2-server-rest-tenant overlay replaces the IDK default URL resolver with one backed by tenant_public_endpoint.

This is what makes the AS work behind CDNs, reverse proxies, and custom domains: the metadata advertises URLs the wallet can actually reach, even when the request lands on the AS through a hostname different from the cluster's own hostname.

For the discovery URLs themselves, the AS supports the spec form (/.well-known/oauth-authorization-server/{tenant-slug}) and the legacy slug-before form (/{tenant-slug}/.well-known/oauth-authorization-server) for OAuth metadata, and the OpenID Discovery spec form (/{tenant-slug}/.well-known/openid-configuration) for OIDC.

Signing Keys

Every AS-issued artifact that needs to be signed (access tokens, id tokens, the AS's own JWKS) is signed with a per-tenant KMS key. The default alias is (tenant, as, access-token-signing); the tenant admin can override the alias through the signing-key admin REST. Rotation is per-tenant and per-alias.

The jwks_uri in the AS metadata resolves to the union of the current and previous signing keys for the tenant, so wallets and downstream services can continue to verify recently-issued tokens during a rotation.

Persistence

Postgres-backed stores in the EDK overlay include:

  • PostgresSigningKeyStore: the per-tenant signing key alias binding.
  • PostgresSingleUseObjectStore: the consolidated single-use object store that covers nonces, JTI replay caches, attestation PoPs, and back-channel logout token caches.
  • PostgresTermsAcceptanceStore: terms-of-service acceptance per principal per tenant.
  • PostgreSqlSessionStorage: the OAuth2 session storage from lib-data-store-session/persistence-oauth2.
  • PostgresAuthenticationSessionStore, PostgresFederationSessionStore, PostgresCredentialStore, PostgresAuthRateLimiter, PostgresBackChannelLogoutRetryQueue, PostgresIdempotencyStore, DatabaseOid4vpAuthSessionStore.
  • PostgresTenantIdpRegistry: the tenant-IdP federation provider registry.

A subset of the AS storage interfaces are still on in-memory implementations as of the current EDK release (the device-flow, attestation-challenge, and PAR session stores in particular). These are tracked for Postgres impls in the phased EDK container roadmap; current deployments that need full multi-replica durability for those flows should consult the deployment notes for the current EDK version.

Building and Running

The Dockerfile is the standard EDK pattern: pre-built fat JAR copied in, eclipse-temurin:21-jre base, non-root appuser, port 8080.

FROM eclipse-temurin:21-jre

RUN groupadd --gid 10001 appuser && \
useradd --uid 10001 --gid appuser --shell /bin/false --create-home appuser

WORKDIR /app

COPY services/oauth2-as/rest/build/libs/*-all.jar ./app.jar
COPY services/oauth2-as/container/config/ ./config/

USER 10001

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

The fat JAR is built from services-oauth2-as-rest with ./gradlew :services-oauth2-as-rest:buildFatJar. The entry point starts the Ktor server (EnterpriseOAuth2AsKtorServer), which installs the tenant resolution plugin first (so tenant context is on the call before the DI graph is consulted) and then installs the Universal HTTP adapters that mount every endpoint command into Ktor.

The minimal application.yaml shipped with the image enables anonymous access on port 8080 for local testing. A production deployment overrides this through environment variables, mounted YAML overlays, and cloud config providers to require admin-scoped bearer JWTs on /api/v1/... and to enable the per-tenant federation, KMS, and Postgres routing wiring.

Operational Notes

  • Token signing latency. Every /token response involves a KMS signing operation. KMS round-trip latency directly translates into token-endpoint p99. For high-throughput deployments, co-locate the AS replica with the KMS, and consider provider backends with low signing latency.
  • Federation resilience. If an upstream IdP is down, the federation authorize step fails. The AS does not silently fall back to another provider. Tenants that need resilience configure multiple providers and let the wallet pick.
  • Pre-auth code TTL. Pre-authorized codes are short-lived single-use objects. A wallet that takes too long between offer acceptance and /token exchange will see the code expire. The default TTL is configurable per tenant.
  • DPoP. When tenants enable DPoP for their clients, the htu claim is checked against the tenant's public-endpoint binding rather than the request host. The same fail-closed default applies, if no binding is configured, DPoP-protected calls are rejected rather than accepted on the request host.