Using Sling OAuth 2.0 client with OIDC support

Logging users into websites securely and managing what they can access can be tricky. OAuth 2.0 and OpenID Connect (OIDC) are well-known solutions for these tasks. At the 2024 AdaptTo conference, Robert Munteanu showed off the Apache Sling OAuth 2.0 client with OIDC support bundle. This new addition for Apache Sling makes it simpler for developers to add secure login and authorization to their Sling applications. In the article we will look into a practical demonstration of this bundle, showcasing an integration with Authentik, an open-source identity provider, to illustrate how Sling applications can leverage these essential protocols for robust and modern user authentication and authorization.

Prerequisites

To begin our development, the following tools are required:

  1. Docker container runtime. I use open-source Colima (https://github.com/abiosoft/colima)
  2. Docker Compose
  3. Java 17 - for the Sling OAuth project build
  4. Java 21 - for the Sling Samples project
  5. Maven 3
  6. Wget

Note: All commands provided in this article are compatible with macOS. Users on other operating systems may need to make adjustments.

Setting Up Identity Provider

For this article I’ve picked Authentik as an Identity Provider. For customer projects we prefer Keycloak, as it’s an Enterprise-grade IdP solution, but here we need something simple and easy to set up and configure as we need. Our first step will be to get a Docker container (https://docs.goauthentik.io/docs/install-config/install/docker-compose) and run it. Pull the docker compose file:

wget https://goauthentik.io/docker-compose.yml

Then generate password and secret key:

echo "PG_PASS=$(openssl rand -base64 36 | tr -d '\n')" >> .env 
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 60 | tr -d '\n')" >> .env

By default Authenitik will use port 9000 for HTTP and 9443 for HTTPS. In my case those ports are already occupied, so I will configure 7000 and 7443 respectively.

echo "COMPOSE_PORT_HTTP=7000" >> .env
echo "COMPOSE_PORT_HTTPS=7443" >> .env

If you would like to use custom certificates, you can put them into the folder “certs” - it will be automatically mounted inside the container. Alternatively you can generate a certificate inside Authentik - that’s what we will do later on. Now we can pull container and run Authentik:

docker-compose pull
docker-compose up

Before we continue with IdP setup, let’s add Authentik and Sling Application-related domains to /etc/hosts file:

# Authentik IDP
127.0.0.1	authentic.local
127.0.0.1	authentic-admin.local

# Sling OAuth
127.0.0.1 sling-oauth.local

With 2 domains we can easily have 2 separate sessions - one admin and one for our Sling application. Now, to perform initial setup of our IdP, we need to access https://authentic-admin.local:7443/if/flow/initial-setup/. After we set admin password, we will see a prompt to create our application:

Let’s create our application:

On the following screen pick “OAuth2/OpenID Provider” and proceed to the next screen. Then pick “default-provider-authorization-explicit-consent (Authorize Application)” for “Authorization flow”. This will make our application to use Authorization Code flow for the OAuth. Client type - “Confidential”, as we will store secrets in our Sling App andthey will not be exposed to the client. Click Next. Skip policies configuration. Review and submit the application configuration.

Now we need to configure Authentik to allow redirects to our Sling Application. Go to Applications -> Providers and in “Redirect URIs/Origins (RegEx)” add regex entry https://sling-oauth.local.*

As we are accessing our Authentik installation via custom domains, let’s also generate an ssl certificate, which will have these domains. Go to System -> Certificates -> Generate and configure it as following:

As it’s a self-signed certificate, we would need to add it to the java’s TrustStore, so our Sling application can communicate with the IdP. At the moment we can expand the generated certificate and download it to do it later. And now we can configure Authentik to use generated certificate for the web server - go to System -> Brands -> “authentik-default” -> “Other global settings” and configure “authentik-web” for the “Web Certificate” field.

To prepare for using our Sling Application, first create a test user in Authentik. Navigate to Directory -> Users -> Create and create a user (in this example, "sling-test-user"). You can set the user's password through the admin UI. Before proceeding with the Sling Application, restart Authentik to apply the Web Certificate changes. Verify that the new certificate is in use before continuing.

Sling OAuth 2.0 client

As Apache Sling OAuth client is still at the early stage of development, I’ve decided to take the latest SNAPSHOT version of it, so first I pulled it with

git clone https://github.com/apache/sling-org-apache-sling-auth-oauth-client.git

and built it, to have the artifact in the local maven repository (project is using Testcontainers library for tests, so if you are using some Docker alternative, make sure to configure several environment variables, as documented on https://java.testcontainers.org/supported_docker_environment/ or skip the tests). For colima it looks like this:

export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock \
&& export TESTCONTAINERS_HOST_OVERRIDE=$(colima ls -j | jq -r '.address') \
&& export DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock" \
&& cd sling-org-apache-sling-auth-oauth-client \
&& mvn clean install

Now we can proceed with our Sling application.

Integrating Authentik with Apache Sling OAuth Demo

We will use Sling Samples as a base for our code, also because it already has a sample for the OAuth. So clone the repository:

git clone https://github.com/apache/sling-samples

We are interested in the OAuth demo project, which you can find inside. Let's start by configuring clientId and clientSecret. This information can be found in the admin interface in Authentik at Applications -> Provider. Pick the provider for our application and edit the configuration - there you can find both values. Now we create in launcher module secrets/authentik folder and put values into the files clientId and clientSecret respectively.

In launcher/src/main/features we will specify our configuration for the service org.apache.sling.auth.oauth_client.impl.OAuthConnectionImpl, which will take care of communication between Sling and IdP:

{
   "configurations": {
       "org.apache.sling.auth.oauth_client.impl.OAuthConnectionImpl~authentik": {
           "name": "authentik",
           "authorizationEndpoint": "https://authentic.local:7443/application/o/authorize/",
           "tokenEndpoint": "https://authentic.local:7443/application/o/token/",
           "clientId": "$[secret:authentik/clientId]",
           "clientSecret": "$[secret:authentik/clientSecret]",
           "scopes": [
               "user:email"
           ]
       }
   }
}

Now we can create our model, which will perform token validation and content fetching, if user is authorized. It can look like next:

@Model(adaptables = SlingHttpServletRequest.class)
public class AuthentikProtectedContent {

   @SlingObject
   private SlingHttpServletRequest request;

   // get a reference to an OSGi service
   @OSGiService(filter = "(name=authentik)")
   private ClientConnection connection;

   @OSGiService
   private OAuthTokenAccess tokenAccess;

   private OAuthTokenResponse tokenResponse;

   @PostConstruct
   public void initToken() {
       tokenResponse = tokenAccess.getAccessToken(connection, request, request.getRequestURI());
   }

   public boolean needsLogin() {
       return !tokenResponse.hasValidToken();
   }

   public URI getLoginUrl() {
       if (needsLogin()) {
           return tokenResponse.getRedirectUri();
       }
       return null;
   }

   public String getProtectedContent() {

       // Take token value tokenResponse.getTokenValue() and access resource which requires it
       return "My Authentik-protected content";
   }
}

Don’t forget to update Sling-Model-Packages configuration of maven bundle plugin, as well as to provide package-info.java. Now we will add the page where we will render our content:

E.g. snippet from my page

<body data-sly-use.contentModel="biz.netcentric.demo.sling.oauth.AuthentikProtectedContent"
     data-sly-set.needsLogin="${contentModel.needsLogin}">
   <main>
       <div data-sly-test="${!needsLogin}">
           ${contentModel.protectedContent}
       </div>
       <div data-sly-test="${needsLogin}">
           <p>Please <a class="underline" href="${contentModel.loginUrl}">login</a> to view the content.</p>
       </div>
   </main>
</body>

Now we can also add a content page under ui.apps/src/main/content/jcr_root/content/oauth-demo, which we will use for the testing.

To use HTTPS for both the Identity Provider (IdP) and the Sling application, additional configuration is required. The Sling application needs an SSL certificate, which can be created by generating a keystore with a self-signed certificate.

keytool -genkeypair \
  -alias sling-oauth \
  -keyalg RSA \
  -keysize 2048 \
  -validity 365 \
  -keystore keystore.jks \
  -storepass changeit \
  -keypass changeit \
  -dname "CN=sling-oauth.local, OU=Dev, O=MyOrg, L=Location, S=State, C=Country"

We will provide the keystore to JVM and configure Sling to use the certificate for the web delivery. We also need to make sure that our certificate used by Authentik is accepted by JVM. To achieve this, we need to convert our certificate to DER format:

openssl x509 -outform der -in authentik-web_certificate.pem -out authentik-web_certificate.der

And generate a key store which will act as a trust store:

keytool -importcert \
  -alias authenik-cert \
  -file my-custom-cert.der \
  -keystore truststore.jks \
  -storepass changeit \
  -noprompt

Now we are ready to build the project with mvn clean install and, finally, run our application. Inside launcher module:

export JAVA_OPTS="-Djavax.net.ssl.trustStore=/Volumes/work/nc/article-sling-oauth/sling-samples/oauth/launcher/truststore.jks -Djavax.net.ssl.trustStorePassword=changeit" && target/dependency/org.apache.sling.feature.launcher/bin/launcher \
   -f target/slingfeature-tmp/feature-app.json \
   -D org.apache.felix.configadmin.plugin.interpolation.secretsdir=secrets \
   -D org.osgi.service.http.port.secure=8443 \
   -D org.apache.felix.https.enable=true \
   -D org.apache.felix.https.keystore=PATH_TO/keystore.jks \
   -D org.apache.felix.https.keystore.password=changeit \
   -D org.apache.felix.https.keystore.key.password=changeit \
   -D org.apache.felix.https.truststore=PATH_TO/truststore.jks \
   -D org.apache.felix.https.truststore.password=changeit

Replace PATH_TO with the path to the generated key stores. Here in this command we:

Alternatively, you can also update a make file and run make run command.

Demo time

We start with https://sling-oauth.local:8443/content/oauth-demo.html where we can login into the Sling app with the default admin/admin credentials. Then we can proceed to the page with our model, which we created above - https://sling-oauth.local:8443/content/oauth-demo/authentik-protected-content.html

Let’s login - we are redirected to the IdP:

And after we complete login, we see our content!

Some words on "Apache Sling OAuth 2.0 client with OIDC support" bundle

The "Apache Sling OAuth 2.0 client with OIDC support" bundle, while in early development, offers core functionalities for common use cases. Recent contributions, such as Redis token storage and an OIDC Authentication Handler, expand its applicability.

Sling OAuth vs AEM Granite OAuth

While AEM's built-in Granite OAuth 2.0 implementation offers extensive functionality due to its longer history, it has certain drawbacks:

The Sling OAuth bundle addresses these limitations and offers a more flexible and extensible approach.

References