SLING-3227 - FormLoginModulePlugin does not work with Oak

Add FormLoginModule based on Oak support for JAAS authentication
-- FormAuthenticationHandler would use FormLoginModule or FormLoginModulePlugin depending on support for Oak LoginModule. This would enable use of same bundle in both Oak and JR2 based Sling deployments
-- For JAAS auth the handler would construct a FormCrendentials instance which is supported by FormLoginModule only. Once validated it would make use of Oak pre auth support to let Oak complete the JAAS login

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1649302 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index e129707..51d34ce 100644
--- a/pom.xml
+++ b/pom.xml
@@ -158,6 +158,19 @@
             <scope>provided</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>oak-core</artifactId>
+            <version>1.0.0</version>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.jaas</artifactId>
+            <version>0.0.2</version>
+            <optional>true</optional>
+        </dependency>
+
         <!-- Test Dependencies -->
         <dependency>
             <groupId>junit</groupId>
diff --git a/src/main/java/org/apache/sling/auth/form/impl/FormAuthenticationHandler.java b/src/main/java/org/apache/sling/auth/form/impl/FormAuthenticationHandler.java
index 6343b04..d14c8c6 100644
--- a/src/main/java/org/apache/sling/auth/form/impl/FormAuthenticationHandler.java
+++ b/src/main/java/org/apache/sling/auth/form/impl/FormAuthenticationHandler.java
@@ -36,6 +36,7 @@
 import javax.servlet.http.HttpSession;
 
 import org.apache.commons.codec.binary.Base64;
+import org.apache.felix.jaas.LoginModuleFactory;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
 import org.apache.felix.scr.annotations.Properties;
@@ -56,6 +57,8 @@
 import org.apache.sling.auth.core.spi.AuthenticationInfo;
 import org.apache.sling.auth.core.spi.DefaultAuthenticationFeedbackHandler;
 import org.apache.sling.auth.form.FormReason;
+import org.apache.sling.auth.form.impl.jaas.FormCredentials;
+import org.apache.sling.auth.form.impl.jaas.JaasHelper;
 import org.apache.sling.commons.osgi.OsgiUtil;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.Constants;
@@ -73,7 +76,12 @@
     @Property(name = Constants.SERVICE_DESCRIPTION, value = "Apache Sling Form Based Authentication Handler"),
     @Property(name = AuthenticationHandler.PATH_PROPERTY, value = "/", cardinality = 100),
     @Property(name = AuthenticationHandler.TYPE_PROPERTY, value = HttpServletRequest.FORM_AUTH, propertyPrivate = true),
-    @Property(name = Constants.SERVICE_RANKING, intValue = 0, propertyPrivate = false) })
+    @Property(name = Constants.SERVICE_RANKING, intValue = 0, propertyPrivate = false),
+
+    @Property(name = LoginModuleFactory.JAAS_CONTROL_FLAG, value = "sufficient"),
+    @Property(name = LoginModuleFactory.JAAS_REALM_NAME, value = "jackrabbit.oak"),
+    @Property(name = LoginModuleFactory.JAAS_RANKING, intValue = 1000)
+})
 @Service
 public class FormAuthenticationHandler extends DefaultAuthenticationFeedbackHandler implements AuthenticationHandler {
 
@@ -295,6 +303,8 @@
      */
     private boolean loginAfterExpire;
 
+    private JaasHelper jaasHelper;
+
     /**
      * Extracts cookie/session based credentials from the request. Returns
      * <code>null</code> if the handler assumes HTTP Basic authentication would
@@ -622,7 +632,13 @@
 
         final AuthenticationInfo info = new AuthenticationInfo(
             HttpServletRequest.FORM_AUTH, userId);
-        info.put(attrCookieAuthData, authData);
+
+        if (jaasHelper.enabled()) {
+            //JcrResourceConstants.AUTHENTICATION_INFO_CREDENTIALS
+            info.put("user.jcr.credentials", new FormCredentials(userId, authData));
+        } else {
+            info.put(attrCookieAuthData, authData);
+        }
 
         return info;
     }
@@ -643,6 +659,8 @@
             if (data instanceof String) {
                 return (String) data;
             }
+        } else if (credentials instanceof FormCredentials){
+            return ((FormCredentials) credentials).getAuthData();
         }
 
         // no SimpleCredentials or no valid attribute
@@ -653,7 +671,7 @@
         return getCookieAuthData(credentials) != null;
     }
 
-    boolean isValid(final Credentials credentials) {
+    public boolean isValid(final Credentials credentials) {
         String authData = getCookieAuthData(credentials);
         if (authData != null) {
             return tokenStore.isValid(authData);
@@ -679,6 +697,7 @@
 
         Dictionary<?, ?> properties = componentContext.getProperties();
 
+        this.jaasHelper = new JaasHelper(this, componentContext.getBundleContext(), properties);
         this.loginForm = OsgiUtil.toString(properties.get(PAR_LOGIN_FORM),
             AuthenticationFormServlet.SERVLET_PATH);
         log.info("Login Form URL {}", loginForm);
@@ -730,12 +749,14 @@
         this.tokenStore = new TokenStore(tokenFile, sessionTimeout, fastSeed);
 
         this.loginModule = null;
-        try {
-            this.loginModule = FormLoginModulePlugin.register(this,
-                componentContext.getBundleContext());
-        } catch (Throwable t) {
-            log.info("Cannot register FormLoginModulePlugin. This is expected if Sling LoginModulePlugin services are not supported");
-            log.debug("dump", t);
+        if (!jaasHelper.enabled()) {
+            try {
+                this.loginModule = FormLoginModulePlugin.register(this,
+                        componentContext.getBundleContext());
+            } catch (Throwable t) {
+                log.info("Cannot register FormLoginModulePlugin. This is expected if Sling LoginModulePlugin services are not supported");
+                log.debug("dump", t);
+            }
         }
 
         this.includeLoginForm = OsgiUtil.toBoolean(properties.get(PAR_INCLUDE_FORM), DEFAULT_INCLUDE_FORM);
@@ -745,6 +766,11 @@
 
     @Deactivate
     protected void deactivate() {
+        if (jaasHelper != null){
+            jaasHelper.close();
+            jaasHelper = null;
+        }
+
         if (loginModule != null) {
             loginModule.unregister();
             loginModule = null;
diff --git a/src/main/java/org/apache/sling/auth/form/impl/jaas/FormCredentials.java b/src/main/java/org/apache/sling/auth/form/impl/jaas/FormCredentials.java
new file mode 100644
index 0000000..6f127fc
--- /dev/null
+++ b/src/main/java/org/apache/sling/auth/form/impl/jaas/FormCredentials.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sling.auth.form.impl.jaas;
+
+import javax.jcr.Credentials;
+
+public class FormCredentials implements Credentials {
+    private final String userId;
+    private final String authData;
+
+    public FormCredentials(String userId, String authData) {
+        this.userId = userId;
+        this.authData = authData;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public String getAuthData() {
+        return authData;
+    }
+}
diff --git a/src/main/java/org/apache/sling/auth/form/impl/jaas/FormLoginModule.java b/src/main/java/org/apache/sling/auth/form/impl/jaas/FormLoginModule.java
new file mode 100644
index 0000000..e98ddf1
--- /dev/null
+++ b/src/main/java/org/apache/sling/auth/form/impl/jaas/FormLoginModule.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sling.auth.form.impl.jaas;
+
+import java.util.Collections;
+import java.util.Set;
+
+import javax.jcr.Credentials;
+import javax.jcr.SimpleCredentials;
+import javax.security.auth.login.LoginException;
+
+import org.apache.jackrabbit.oak.spi.security.authentication.AbstractLoginModule;
+import org.apache.jackrabbit.oak.spi.security.authentication.PreAuthenticatedLogin;
+import org.apache.sling.auth.form.impl.FormAuthenticationHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+final class FormLoginModule extends AbstractLoginModule {
+    private static final Logger log = LoggerFactory.getLogger(FormLoginModule.class);
+
+    /**
+     * The set of supported credentials. required by the {@link org.apache.jackrabbit.oak.spi.security.authentication.AbstractLoginModule}
+     */
+    private static final Set<Class> SUPPORTED_CREDENTIALS = Collections.<Class>singleton(FormCredentials.class);
+    private static final char[] EMPTY_PWD = new char[0];
+
+    /**
+     * Extracted userId during login call.
+     */
+    private String userId;
+
+    @Override
+    protected Set<Class> getSupportedCredentials() {
+        return SUPPORTED_CREDENTIALS;
+    }
+
+    /**
+     * The {@link org.apache.sling.auth.form.impl.FormAuthenticationHandler} used to validate the credentials
+     * and its contents.
+     */
+    private final FormAuthenticationHandler authHandler;
+
+    FormLoginModule(FormAuthenticationHandler authHandler) {
+        this.authHandler = authHandler;
+    }
+
+    @SuppressWarnings("unchecked")
+    public boolean login() throws LoginException {
+        Credentials credentials = getCredentials();
+        if (credentials instanceof FormCredentials) {
+            FormCredentials cred = (FormCredentials) credentials;
+            userId = cred.getUserId();
+
+            if (!authHandler.isValid(cred)){
+                log.debug("Invalid credentials");
+                return false;
+            }
+
+            if (userId == null) {
+                log.debug("Could not extract userId/credentials");
+            } else {
+                // we just set the login name and rely on the following login modules to populate the subject
+                sharedState.put(SHARED_KEY_PRE_AUTH_LOGIN, new PreAuthenticatedLogin(userId));
+                sharedState.put(SHARED_KEY_CREDENTIALS, new SimpleCredentials(userId, EMPTY_PWD));
+                sharedState.put(SHARED_KEY_LOGIN_NAME, userId);
+                log.debug("login succeeded with trusted user: {}", userId);
+            }
+        }
+        return false;
+    }
+
+    public boolean commit() throws LoginException {
+        if (userId == null) {
+            // login attempt in this login module was not successful
+            clearState();
+        }
+        return false;
+    }
+
+    @Override
+    protected void clearState() {
+        userId = null;
+        super.clearState();
+    }
+}
diff --git a/src/main/java/org/apache/sling/auth/form/impl/jaas/JaasHelper.java b/src/main/java/org/apache/sling/auth/form/impl/jaas/JaasHelper.java
new file mode 100644
index 0000000..bb5a1b5
--- /dev/null
+++ b/src/main/java/org/apache/sling/auth/form/impl/jaas/JaasHelper.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sling.auth.form.impl.jaas;
+
+import java.util.Dictionary;
+
+import javax.security.auth.spi.LoginModule;
+
+import org.apache.felix.jaas.LoginModuleFactory;
+import org.apache.sling.auth.form.impl.FormAuthenticationHandler;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class JaasHelper {
+
+    private static final Logger log = LoggerFactory.getLogger(JaasHelper.class);
+
+    private final FormAuthenticationHandler authHandler;
+
+    /**
+     * login module service registration
+     */
+    private final ServiceRegistration factoryRegistration;
+
+    /**
+     * Opens/Initializes the helper and registers the login module factory (LMF) service if possible.
+     *
+     * @param ctx        the bundle context
+     * @param properties properties that contain the jaas related LMF service properties.
+     */
+    public JaasHelper(FormAuthenticationHandler authHandler, BundleContext ctx, Dictionary properties) {
+        this.authHandler = authHandler;
+        // we dynamically register the LoginModuleFactory for the case we detect a login module.
+        if (hasSSOLoginModule(ctx)) {
+            factoryRegistration = registerLoginModuleFactory(ctx, properties);
+        } else {
+            factoryRegistration = null;
+        }
+    }
+
+    /**
+     * Checks if JAAS support is enabled and the SSO login module is present.
+     *
+     * @return {@code true} if JAAS support is enabled.
+     */
+    public boolean enabled() {
+        return factoryRegistration != null;
+    }
+
+
+    /**
+     * Closes this helper and unregisters the login module factory if needed.
+     */
+    public void close() {
+        if (factoryRegistration != null) {
+            factoryRegistration.unregister();
+        }
+    }
+
+    private ServiceRegistration registerLoginModuleFactory(BundleContext ctx, Dictionary properties) {
+        ServiceRegistration reg = null;
+        try {
+            java.util.Properties props = new java.util.Properties();
+            final String desc = "LoginModule Support for FormAuthenticationHandler";
+            props.put(Constants.SERVICE_DESCRIPTION, desc);
+            props.put(Constants.SERVICE_VENDOR, ctx.getBundle().getHeaders().get(Constants.BUNDLE_VENDOR));
+
+            props.put(LoginModuleFactory.JAAS_RANKING, properties.get(LoginModuleFactory.JAAS_RANKING));
+            props.put(LoginModuleFactory.JAAS_CONTROL_FLAG, properties.get(LoginModuleFactory.JAAS_CONTROL_FLAG));
+            props.put(LoginModuleFactory.JAAS_REALM_NAME, properties.get(LoginModuleFactory.JAAS_REALM_NAME));
+            reg = ctx.registerService(LoginModuleFactory.class.getName(),
+                    new LoginModuleFactory() {
+                        public LoginModule createLoginModule() {
+                            return new FormLoginModule(authHandler);
+                        }
+
+                        @Override
+                        public String toString() {
+                            return desc + " (" +FormLoginModule.class.getName()+")";
+                        }
+                    },
+                    props
+            );
+            log.info("Registered FormLoginModuleFactory");
+        } catch (Throwable e) {
+            log.error("unable to create an register the SSO login module factory", e);
+        }
+        return reg;
+    }
+
+    /**
+     * Checks if the {@link org.apache.sling.auth.form.impl.jaas.FormLoginModule} is available. This would not be the case
+     * in an non-oak setup. Note this only checks if the login module can be loaded, not if it is actually enabled
+     * in the jaas config.
+     *
+     * @return {@code true} if the SSOLoginModule is available.
+     */
+    private static boolean hasSSOLoginModule(BundleContext ctx) {
+        try {
+            ctx.getBundle().loadClass("org.apache.sling.auth.form.impl.jaas.FormLoginModule");
+            log.debug("FormLoginModule available.");
+            return true;
+        } catch (Throwable e) {
+            log.debug("no FormLoginModule available.", e);
+        }
+        return false;
+    }
+}
diff --git a/src/main/resources/OSGI-INF/metatype/metatype.properties b/src/main/resources/OSGI-INF/metatype/metatype.properties
index 2d92666..7070cdb 100644
--- a/src/main/resources/OSGI-INF/metatype/metatype.properties
+++ b/src/main/resources/OSGI-INF/metatype/metatype.properties
@@ -91,4 +91,19 @@
 form.default.cookie.domain.description = The domain on which authentication cookies will \
  be set, unless overridden in the AuthenticationInfo object. The default is null \
  which means to set the cookie on the request domain.
- 
\ No newline at end of file
+
+jaas.controlFlag.name = JAAS Control Flag
+jaas.controlFlag.description = Property name specifying whether or not a LoginModule is REQUIRED, REQUISITE, SUFFICIENT \
+  or OPTIONAL. Refer to the JAAS configuration documentation for more details around the meaning of these flags. \
+  Jackrabbit Oak only.
+
+jaas.realmName.name = JAAS Realm
+jaas.realmName.description = Property name specifying the realm name (or application name) against which the LoginModule \
+  is be registered. If no realm name is provided then LoginModule is registered with a default realm as configured in \
+  the Felix JAAS configuration. \
+  Jackrabbit Oak only.
+
+jaas.ranking.name = JAAS Ranking
+jaas.ranking.description = Property name specifying the ranking (i.e. sort order) of the configured login module \
+  entries. The entries are sorted in a descending order (i.e. higher value ranked configurations come first). \
+  Jackrabbit Oak only.