chore: build on Java 17 and 21 only
[!IMPORTANT] The Java APIs exported by this bundle are considered experimental and are marked as being @ProviderType. Expect them to change frequently until they are considered stable.
This bundle adds support for Sling-based applications to function as an OAuth 2.0 client (RFC 6749) and implements the basis for being an Open ID connect relying party.
Its main objective is to simplify access to id and access tokens in a secure manner. It currently supports the authentication code flow based on OIDC and OAuth 2.0 .
The bundle exposes an abstract OAuthEnabledSlingServlet that contains the boilerplate code needed to obtain a valid OAuth 2 access token.
Basic usage is as follows
import org.apache.sling.auth.oauth_client.*; @Component(service = { Servlet.class }) @SlingServletPaths(value = "/bin/myservlet") public class MySlingServlet extends OAuthEnabledSlingServlet { private final MyRemoteService svc; @Activate public MySlingServlet(@Reference OidcConnection connection, @Reference OAuthTokenStore tokenStore, @Reference OAuthTokenRefresher oidcClient, @Reference MyRemoteService svc) { super(connection, tokenStore, oidcClient); this.svc = svc; } @Override protected void doGetWithToken(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response, OAuthToken token) throws IOException, ServletException { this.csv.query("my-query", token.getValue()).writeResponseTo(response.getOutputStream()); } }
TODO
If an access token response contains an expiry date the bundle will make sure that it is not accessible via APIs. This will not cover all scenarios because access tokens can expire or be invalidated out of band.
The client will need to determine if the access token is invalid as this is a provider-specific check.
To remove invalid access tokens there is a low-level API
public class MyComponent {} @Reference private OAuthTokenStore tokenStore; public void execute(@Reference OidcConnection connection, ResourceResolver resolver) { // code elided if ( accessTokenIsInvalid() ) { tokenStore.clearAccessToken(connection, resolver); // redirect to provider or present a message to the user } } }
For classes that extend from the OAuthEnabledSlingServlet the following method override can be applied
@Component(service = { Servlet.class }) @SlingServletPaths(value = "/bin/myservlet") public class MySlingServlet extends OAuthEnabledSlingServlet { // other methods elided @Override protected boolean isInvalidAccessTokenException(Exception e) { return e.getCause() instanceof InvalidAccessTokenException; } }
The top-level servlets used for the OAuth flow will validate parameters that are expected to be sent by the client and return a status code of 400 in case the parameters are missing or invalid.
For others problems related to the OAuth flow these servlets throw specific subclasses of ServletException. The exceptions will return generic messages that can be displayed directly to the user and store the actual cause in nested exception so that it is logged.
These exceptions are:
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 that applications install specific error handlers for these exceptions. See the Apache Sling error handling documentation for more details.
Client registration is specific to each provider. When registering, note the following:
Validated providers:
A set of dependencies required by this bundle, on top of the Sling Starter ones, is available at src/main/features/main.json.
Since the bundle relies on encryption to create and validate the OAuth 2.0 state parameter, a 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 names property is important since it is used to select the CryptoService used by this bundle.
In addition, one of the following types of OSGi configuration must be added:
"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"] }
"org.apache.sling.auth.oauth_client.impl.OAuthConnectionImpl~github": { "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"] }
At this point, the OAuth process can be kicked of by navigating to http://localhost:8080/system/sling/oauth/entry-point?c=provider
The tokens can be stored either in the JCR repository, under the user's home, or in Redis. A configuration is required to select a provider.
The tokens are stored under the user's home, under the oauth-tokens/$PROVIDER_NAME node.
"org.apache.sling.auth.oauth_client.impl.JcrUserHomeOAuthTokenStore" : { }
"org.apache.sling.auth.oauth_client.impl.RedisOAuthTokenStore" : { "redisUrl": "redis://localhost:6379" }
mvn clean installmvn feature-launcher:start feature-launcher:stop -Dfeature-launcher.waitForInputexport CLIENT_SECRET=$(cat src/test/resources/keycloak-import/sling.json | jq --raw-output '.clients[] | select (.clientId == "oidc-test") | .secret')
$ curl -u admin:admin -X POST -d "apply=true" -d "propertylist=name,baseUrl,clientId,clientSecret,scopes" \
-d "name=keycloak-dev" \
-d "baseUrl=http://localhost:8081/realms/sling" \
-d "clientId=oidc-test"\
-d "clientSecret=$CLIENT_SECRET" \
-d "scopes=openid" \
-d "factoryPid=org.apache.sling.auth.oauth_client.impl.OidcConnectionImpl" \
http://localhost:8080/system/console/configMgr/org.apache.sling.auth.oauth_client.impl.OidcConnectionImpl~keycloak-dev
Now you can
Note that this imports the test setup with a single user with a redirect_uri set to http://localhost*, which can be a security issue.
$ docker run --rm --volume $(pwd)/src/test/resources/keycloak-import:/opt/keycloak/data/import -p 8081:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:20.0.3 start-dev --import-realm
$ docker run --rm --volume $(pwd)/keycloak-data:/opt/keycloak/data -p 8081:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:20.0.3 start-dev
$ docker run --rm --volume (pwd)/keycloak-data:/opt/keycloak/data -p 8081:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:20.0.3 export --realm sling --users realm_file --file /opt/keycloak/data/export/sling.json