Apache Sling OAuth 2.0 client with OIDC support
July 16, 2026 ยท View on GitHub
Apache Sling OAuth 2.0 client with OIDC support
Important
The Java APIs exported by this bundle are considered experimental and are marked as
@ProviderType. The APIs may change in an incompatible way in future minor releases.
This bundle adds support for Sling-based applications to function as an OAuth 2.0 client (RFC 6749) and as an OpenID Connect relying party.
It focuses on secure access to OAuth/OIDC tokens and supports authorization code flow for both OAuth 2.0 and OIDC.
Build and test
- Build bundle and run tests:
mvn clean install - Unit tests only:
mvn test - Full build including integration tests (Docker required):
mvn verify - Skip integration tests:
mvn install -DskipITs - Disable Keycloak-based ITs explicitly:
mvn verify -Dit.keycloak.enabled=false - Run one unit test class:
mvn test -Dtest=OidcConnectionImplTest - Run one integration test:
mvn verify -Dit=AuthorizationCodeFlowIT
Project layout
src/
main/
features/
main.json # Base feature model
redis.json # Optional Redis token store feature overlay
java/
org/apache/sling/auth/oauth_client/
*.java # Public API
impl/ # OSGi DS components (internal)
spi/ # Extension points for auth integration
support/ # Consumer helper base classes
test/
java/
.../itbundle/ # IT-only support bundle generation/install helpers
resources/
keycloak-import/ # Local dev / IT realm data
Usage
Models and other Java APIs
The OAuthTokenAccess OSGi service exposes methods to retrieve and clear access tokens. These methods encapsulate
persistence concerns and handle refresh tokens transparently, when present.
@Model(adaptables = SlingHttpServletRequest.class)
public class MyModel {
@SlingObject private SlingHttpServletRequest request;
@OSGiService(filter = "(name=foo)") private ClientConnection connection;
@OSGiService private OAuthTokenAccess tokenAccess;
private OAuthTokenResponse tokenResponse;
@PostConstruct
public void initToken() {
tokenResponse = tokenAccess.getAccessToken(connection, request, request.getRequestURI());
}
public MyView getResponse() {
if (tokenResponse.hasValidToken()) {
return doQuery(tokenResponse.getTokenValue());
}
return null;
}
public String getRedirectLink() {
if (!tokenResponse.hasValidToken()) {
return tokenResponse.getRedirectUri().toString();
}
return null;
}
}
Servlets
The bundle exposes an abstract OAuthEnabledSlingServlet that handles token retrieval/refresh and redirects to the OAuth/OIDC flow when needed.
import java.io.IOException;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.auth.oauth_client.ClientConnection;
import org.apache.sling.auth.oauth_client.OAuthTokenAccess;
import org.apache.sling.auth.oauth_client.support.OAuthEnabledSlingServlet;
import org.apache.sling.servlets.annotations.SlingServletPaths;
import org.jetbrains.annotations.NotNull;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@Component(service = Servlet.class)
@SlingServletPaths("/bin/myservlet")
public class MySlingServlet extends OAuthEnabledSlingServlet {
private final MyRemoteService svc;
@Activate
public MySlingServlet(
@Reference ClientConnection connection,
@Reference OAuthTokenAccess tokenAccess,
@Reference MyRemoteService svc) {
super(connection, tokenAccess);
this.svc = svc;
}
@Override
protected void doGetWithToken(
@NotNull SlingHttpServletRequest request,
@NotNull SlingHttpServletResponse response,
String accessToken) throws IOException, ServletException {
this.svc.query("my-query", accessToken).writeResponseTo(response.getOutputStream());
}
}
OIDC authentication handler
This bundle also ships OidcAuthenticationHandler for Sling Auth Core integration.
Notable capabilities:
redirectrequest parameter support to return users to a specific local path after authentication- Resource Indicators support (
resource) for RFC 8707 - Configurable max age for the transient
sling.oauth-request-keycookie (requestKeyCookieMaxAgeSeconds) - Optional PKCE support (
pkceEnabled) - Optional SP-initiated logout support (
enableSPInitiatedSingleLogout) with host allow-list enforcement (logoutRedirectAllowedHosts)
Clearing access tokens
If an access token response contains an expiry date, the bundle makes sure expired tokens are not returned by APIs. This does not cover out-of-band invalidation at the provider, so clients still need provider-specific invalid-token handling.
When the request/response are available
This is generally recommended because it can return a redirect URI that starts a new OAuth authorization flow.
@Model(adaptables = SlingHttpServletRequest.class)
public class MySlingModel {
@OSGiService private OAuthTokenAccess tokenAccess;
@SlingObject SlingHttpServletRequest request;
@OSGiService(filter = "(name=foo)") private ClientConnection connection;
public String getLink() {
// code elided
if (accessTokenIsInvalid()) {
OAuthTokenResponse response = tokenAccess.clearAccessToken(connection, request, request.getRequestURI());
return response.getRedirectUri().toString();
}
return null;
}
}
When request/response are not available
Use this for background or non-interactive invalidation where no redirect URI is needed.
@Component
public class MyComponent {
@Reference private OAuthTokenAccess tokenAccess;
public void execute(@Reference ClientConnection connection, ResourceResolver resolver) {
// code elided
if (accessTokenIsInvalid()) {
tokenAccess.clearAccessToken(connection, resolver);
}
}
}
When extending OAuthEnabledSlingServlet
For subclasses of OAuthEnabledSlingServlet, override isInvalidAccessTokenException. If it returns true, the token is cleared and a new OAuth flow starts.
@Component(service = Servlet.class)
@SlingServletPaths("/bin/myservlet")
public class MySlingServlet extends OAuthEnabledSlingServlet {
// other methods elided
@Override
protected boolean isInvalidAccessTokenException(Exception e) {
return e.getCause() instanceof InvalidAccessTokenException;
}
}
Error handling
Top-level OAuth servlets validate required parameters and return HTTP 400 for missing/invalid inputs.
For other OAuth flow problems, these servlets throw specific ServletException subclasses with user-safe messages and nested root causes for logging:
org.apache.sling.auth.oauth_client.impl.OAuthCallbackExceptionorg.apache.sling.auth.oauth_client.impl.OAuthEntryPointExceptionorg.apache.sling.auth.oauth_client.impl.OAuthFlowException(superclass)
It is recommended to install dedicated error handlers for these exceptions. See Apache Sling error handling documentation.
Client registration
Client registration is provider-specific. At minimum:
- Register callback URL as
$HOST/system/sling/oauth/callback(for local development typicallyhttp://localhost:8080/system/sling/oauth/callback) - Capture provider client ID and client secret
- Define scopes your application needs
Validated providers include:
- Google (OIDC) via
https://accounts.google.com - GitHub (OAuth 2.0) via
https://github.com/login/oauth/authorizeandhttps://github.com/login/oauth/access_token - Keycloak
- Microsoft (OIDC) via
https://login.microsoftonline.com/$TENANT_ID/v2.0 - Adobe IMS (OAuth 2.0) via
https://ims-na1.adobelogin.com/ims/authorize/v3andhttps://ims-na1.adobelogin.com/ims/token/v1
Deployment
Base bundle dependencies (on top of Sling Starter) are defined in src/main/features/main.json.
Additional dependencies for Redis token storage are in src/main/features/redis.json.
Redis support is optional at runtime (redis.clients.jedis is imported as optional), so the bundle also works without Redis on the classpath.
CryptoService configuration
Because OAuth state values are encrypted/signed, CryptoService must be configured:
"org.apache.sling.commons.crypto.internal.FilePasswordProvider~oauth": {
"path": "secrets/encrypt/password",
"fix.posixNewline": true
},
"org.apache.sling.commons.crypto.jasypt.internal.JasyptRandomIvGeneratorRegistrar~oauth": {
"algorithm": "SHA1PRNG"
},
"org.apache.sling.commons.crypto.jasypt.internal.JasyptStandardPbeStringCryptoService~oauth": {
"names": ["sling-oauth"],
"algorithm": "PBEWITHHMACSHA512ANDAES_256"
}
The sling-oauth name is required because this bundle selects the crypto service by that name.
Client connection configuration
Configure one (or more) client connections:
OIDC variant (OidcConnectionImpl)
Configure OIDC either with baseUrl metadata discovery or by explicitly setting all endpoints (authorizationEndpoint, tokenEndpoint, userInfoUrl, jwkSetURL, issuer) โ but not both.
"org.apache.sling.auth.oauth_client.impl.OidcConnectionImpl~provider": {
"name": "provider",
"baseUrl": "https://example.com",
"clientId": "$[secret:provider/clientId]",
"clientSecret": "$[secret:provider/clientSecret]",
"scopes": ["openid"],
"additionalAuthorizationParameters": ["prompt=consent"]
}
For SP-initiated logout, endSessionEndpoint can also be configured (or discovered from OIDC metadata when available).
OAuth variant (OAuthConnectionImpl)
"org.apache.sling.auth.oauth_client.impl.OAuthConnectionImpl~provider": {
"name": "provider",
"authorizationEndpoint": "https://example.com/login/oauth/authorize",
"tokenEndpoint": "https://example.com/login/oauth/access_token",
"clientId": "$[secret:provider/clientId]",
"clientSecret": "$[secret:provider/clientSecret]",
"scopes": ["user:email"],
"additionalAuthorizationParameters": ["allow_signup=false"]
}
Start the OAuth entry-point flow at:
http://localhost:8080/system/sling/oauth/entry-point?c=provider
OIDC Authentication Handler configuration (optional)
If you use OidcAuthenticationHandler as your auth mechanism, configure at least:
"org.apache.sling.auth.oauth_client.impl.OidcAuthenticationHandler~provider": {
"path": ["/"],
"idp": "oidc",
"callbackUri": "http://localhost:8080/system/sling/oauth/callback",
"defaultConnectionName": "provider",
"pkceEnabled": true,
"userInfoEnabled": true,
"resource": ["https://api.example.com"],
"requestKeyCookieMaxAgeSeconds": 300,
"enableSPInitiatedSingleLogout": false,
"logoutRedirectPath": "/",
"logoutRedirectAllowedHosts": ["localhost"]
}
When enableSPInitiatedSingleLogout=true, logoutRedirectAllowedHosts is mandatory for open-redirect protection.
Token storage
Tokens can be stored in JCR (under user home) or Redis.
JCR storage
Tokens are stored at oauth-tokens/$PROVIDER_NAME under the user home.
"org.apache.sling.auth.oauth_client.impl.JcrUserHomeOAuthTokenStore": {}
Redis storage
"org.apache.sling.auth.oauth_client.impl.RedisOAuthTokenStore": {
"redisUrl": "redis://localhost:6379"
}
Local development setup
Quickstart
- Start Keycloak with the prebuilt test realm:
make keycloak-run-import
- Build once:
mvn clean install -DskipITs
- Start Sling:
mvn feature-launcher:start feature-launcher:stop -Dfeature-launcher.waitForInput- or
make sling-run
- Create OIDC connection config in Sling:
make sling-create-config
Then:
- Keycloak:
http://localhost:8081 - Sling:
http://localhost:8080 - OAuth entry point:
http://localhost:8080/system/sling/oauth/entry-point?c=keycloak-dev
Integration test notes
- Integration tests use Testcontainers (Keycloak + Redis) and require Docker.
- During ITs, Sling runs on a reserved random port (
http.port) to avoid collisions; do not assume8080. - Use
-Dit.startTimeoutSeconds=60(default) to control Sling startup wait in ITs. - You can reuse an already running Keycloak by setting
KEYCLOAK_URLinstead of starting a Testcontainers Keycloak. - To skip only Keycloak-based integration tests, use
-Dit.keycloak.enabled=false. - Note: ITs use Keycloak
quay.io/keycloak/keycloak:26.4, whilemake keycloak-run-importusesquay.io/keycloak/keycloak:20.0.3for local manual setup.
Security
The project threat model is documented at https://github.com/apache/sling/blob/master/docs/threat-model.md .
