Apache Sling OAuth client

Clone this repo:
  1. deb3558 docs: expand AGENTS.md and README.md with IT and connection details (#50) by Carsten Ziegeler · 13 days ago master
  2. 2d7d0c3 docs: update AGENTS.md and README with project layout, gotchas, and API notes (#49) by Carsten Ziegeler · 13 days ago
  3. 881f98c Add security section to AGENTS.md by Robert Munteanu · 8 weeks ago
  4. 80ed4ef docs: rewrite README and AGENTS.md for clarity and completeness (#48) by Carsten Ziegeler · 8 weeks ago
  5. e67e8dd chore(deps-dev): bump org.assertj:assertj-core from 3.24.2 to 3.27.7 (#47) by dependabot[bot] · 9 weeks ago

Apache Sling

Build Status Test Status Sonarcloud Status auth License

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:

  • redirect request 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-key cookie (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.OAuthCallbackException
  • org.apache.sling.auth.oauth_client.impl.OAuthEntryPointException
  • org.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 typically http://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/authorize and https://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/v3 and https://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

  1. Start Keycloak with the prebuilt test realm:
    • make keycloak-run-import
  2. Build once:
    • mvn clean install -DskipITs
  3. Start Sling:
    • mvn feature-launcher:start feature-launcher:stop -Dfeature-launcher.waitForInput
    • or make sling-run
  4. 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 assume 8080.
  • Use -Dit.startTimeoutSeconds=60 (default) to control Sling startup wait in ITs.
  • You can reuse an already running Keycloak by setting KEYCLOAK_URL instead 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, while make keycloak-run-import uses quay.io/keycloak/keycloak:20.0.3 for local manual setup.

Security

The project threat model is documented at https://github.com/apache/sling/blob/master/docs/threat-model.md .