SLING-12714 - Impementation of OidcAuthenticationHandler (#14) * minor improvements: add nullable and notnull annotations, defense against NPE * docs: record Microsoft OIDC as validated * first commit of auth handler * fix on main.json * fix on main.json * auth handler provision users and authenticate users. Profile not correcly provisioned. * added redirect management * Set up Maven build with GH actions (#2) * modified org.apache.jackrabbit version and fix on main.json as suggested by rmuntean * Added TokenUpdate service to allow to perform custom update on login token. * Fixed IT Test. * renamed TokenUpdate class to LoginCookieManager * Exported spi, defined serviceRank, added support for groups * minor improvements: add nullable and notnull annotations, defense against NPE * Set up Maven build with GH actions (#2) * ci: only run the test phase The package phase downloads the SNAPSHOT version of the Sling Starter and becomes rate limited. * Adding AuthenticationHandler (#1) * docs: record Microsoft OIDC as validated * first commit of auth handler * fix on main.json * fix on main.json * auth handler provision users and authenticate users. Profile not correcly provisioned. * added redirect management * modified org.apache.jackrabbit version and fix on main.json as suggested by rmuntean * Added TokenUpdate service to allow to perform custom update on login token. * Fixed IT Test. --------- Co-authored-by: Robert Munteanu <rombert@apache.org> * fix imports after merge, removed old files * removed OidcBearerTokenAuthenticationHandler * Exported some JcrUserHomeOAuthTokenStore and required dependencies interfaces and classes * Added support for pkce in Authorization Code flow * Revert "Added support for pkce in Authorization Code flow" This reverts commit baa622c083db53635476974d7b9e8dcd3d3095e7. * Removed classes added to spi to reimplement services that uses sling-common-crypto * Added first implementation of OidcAuthenticationHandlerTest * - Implemented Oidc ID Token validation - Added Unit Test to OidcAuthenticationHandler - Added configuration parameter to disable UserInfo - Updated interface in UserInfoProcessor * - fix on the UserInfoProcessor interface to pass openid subject - fix on Unit Test flaky * fix on pom after robert's comment * Added more Unit Tests Improved LoginCookieManager interface * Added RetryRule for IT Tests * Added retry in the first get in IT Tests * Removed parameter in javadoc * Added new tests * Fixed retry * Increased Unit Test with line coverage up to 81% * small changes * - removed old Felix SCR annotations - small increase on test coverage * Increased test coverage for OidcIdentityProvider * small changes to increase readability of the patch * small change on pom.xml to increase readability of the patch * address review findings * fix OAuthToken.getValue such that it never returns null -> fix potential NPE and remove corresponding FIXME comments * avoid NPE with OauthTokens.getAccessToken return null, add TODOs to clarify expected behavior in case of null tokens and upon 'clearAccessToken' * minor improvements to tests, SlingLoginCookieManager: make sure timeout is initialized before passing it to TokenStore constructor * simplify OidcAuthenticationHandler.extractCredentials (first round) * OidcAuthenticationHandler: making COOKIE_MAX_AGE_SECONDS constant private, rearraning constants according to java coding style * simplify OidcAuthenticationHandler.extractCredentials (second round) * resolve potential NPE in UserInfoProcessorImpl due to null access token, move access-token-attribute name constant from impl to OAuthTokenStore * improve SlingLoginCookieManagerTest: fill in missing tests, remove empty tests for private methods, address minor sonar findings * extract RedirectTarget to avoid duplication and auth-handler depending on OAuthEntryPointServlet, minor improvements to tests * minor improvements: test coverage, nullability annotations, javadoc * minor improvements: tests * TokenStoreTest: run parametrized for better coverage of fastSeed false and true --------- Co-authored-by: angela <anchela@adobe.com> Co-authored-by: Robert Munteanu <rombert@apache.org> Co-authored-by: Robert Munteanu <robert.munteanu@gmail.com>
[!IMPORTANT] The Java APIs exported by this bundle are considered experimental and are marked as being @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 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 OAuthTokenAccess OSGi service exposes methods to retrieve and clear access tokens. These methods encapsulate persistence concerns and handle refresh tokens transparently, if 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; } }
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 OAuthTokenAccess tokenAccess, @Reference MyRemoteService svc) { super(connection, tokenAccess); 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()); } }
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.
This method is generally recommended as it permits the generation of a redirect URI that will kick off a new OAuth authorisation 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(); } } }
This approach should be used when invalidating access tokens without user interaction, as it does not provide a mechanism to generate a redirect URL for restarting the OAuth authorisation flow and obtaining a new access token.
@Component public class MyComponent { @Reference private OAuthTokenAccess tokenAccess; public void execute(@Reference OidcConnection connection, ResourceResolver resolver) { // code elided if ( accessTokenIsInvalid() ) { tokenAccess.clearAccessToken(connection, resolver); } } }
For classes that extend from the OAuthEnabledSlingServlet the isInvalidAccessTokenException method can be overriden. If this method returns true, the access token is cleared and a new OAuth flow is started.
@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. For the tokens to be stored in Redis ( see [#redis-storage] ) an additional feature with dependencies is found at src/main/features/redis.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
Create a directory to store the exported realm
mkdir $(pwd)/keycloak-data/export
Export the 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 export --realm sling --users realm_file --file /opt/keycloak/data/export/sling.json