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:
- Docker container runtime. I use open-source Colima (https://github.com/abiosoft/colima)
- Docker Compose
- Java 17 - for the Sling OAuth project build
- Java 21 - for the Sling Samples project
- Maven 3
- 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:
- Name: sling-oauth
- Slug: sling-oauth
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:
Common Name
:authentik-web
- For
Subject-alt name
put our 2 custom domains: authentic.local,authentic-admin.local - Keep default values for the rest of the fields.
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:
- Go to
ui.apps/src/main/content/jcr_root/apps/oauth-demo/components
- Create page (you can use existing GitHub example)
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:
- pass our trust store with the certificate used by Authentik to the JVM
- pass the feature model to Sling Launcher
- pass https-related configuration to Apache Felix Jetty Based Http Service, where we enable HTTPS and provide a keystore with the certificate we generated above.
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:
- no support for refresh tokens
- access tokens that are no longer considered after user creation and are stored in the repository
- a limitation of only one configurable OAuth 2.0 provider. Implementing additional custom providers requires custom development.
The Sling OAuth bundle addresses these limitations and offers a more flexible and extensible approach.