001    /**
002     *  Licensed to the Apache Software Foundation (ASF) under one or more
003     *  contributor license agreements.  See the NOTICE file distributed with
004     *  this work for additional information regarding copyright ownership.
005     *  The ASF licenses this file to You under the Apache License, Version 2.0
006     *  (the "License"); you may not use this file except in compliance with
007     *  the License.  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    
018    package org.apache.geronimo.security.realm.providers;
019    
020    import java.io.IOException;
021    import java.security.Principal;
022    import java.security.cert.Certificate;
023    import java.security.cert.X509Certificate;
024    import java.util.Arrays;
025    import java.util.Collections;
026    import java.util.HashSet;
027    import java.util.List;
028    import java.util.Map;
029    import java.util.Set;
030    import javax.security.auth.Subject;
031    import javax.security.auth.callback.Callback;
032    import javax.security.auth.callback.CallbackHandler;
033    import javax.security.auth.callback.UnsupportedCallbackException;
034    import javax.security.auth.login.FailedLoginException;
035    import javax.security.auth.login.LoginException;
036    import javax.security.auth.spi.LoginModule;
037    import javax.security.auth.x500.X500Principal;
038    
039    import org.apache.commons.logging.Log;
040    import org.apache.commons.logging.LogFactory;
041    import org.apache.geronimo.security.jaas.JaasLoginModuleUse;
042    import org.apache.geronimo.security.jaas.WrappingLoginModule;
043    
044    
045    /**
046     * An example LoginModule that authenticates based on a client certificate.
047     * Authentication is provided by the SSL layer supplying the client certificate.
048     * All we check is that it is present. Expects
049     * to be run by a GenericSecurityRealm (doesn't work on its own).
050     *
051     * This login module checks security credentials so the lifecycle methods must return true to indicate success
052     * or throw LoginException to indicate failure.
053     *
054     * @version $Rev: 706640 $ $Date: 2008-10-21 14:44:05 +0000 (Tue, 21 Oct 2008) $
055     */
056    public class CertificateChainLoginModule implements LoginModule {
057        private static Log log = LogFactory.getLog(CertificateChainLoginModule.class);
058    
059        // Note: If this LoginModule supports any options, the Collections.EMPTY_LIST in the following should be
060        // replaced with the list of supported options for e.g. Arrays.asList(option1, option2, ...) etc.
061        public final static List<String> supportedOptions = Collections.unmodifiableList(Collections.EMPTY_LIST);
062        private Subject subject;
063        private CallbackHandler handler;
064        private X500Principal principal;
065        private boolean loginSucceeded;
066        private final Set<Principal> allPrincipals = new HashSet<Principal>();
067    
068        public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
069            this.subject = subject;
070            this.handler = callbackHandler;
071            for(Object option: options.keySet()) {
072                if(!supportedOptions.contains(option) && !JaasLoginModuleUse.supportedOptions.contains(option)
073                        && !WrappingLoginModule.supportedOptions.contains(option)) {
074                    log.warn("Ignoring option: "+option+". Not supported.");
075                }
076            }
077        }
078    
079        /**
080         * This LoginModule is not to be ignored.  So, this method should never return false.
081         * @return true if authentication succeeds, or throw a LoginException such as FailedLoginException
082         *         if authentication fails
083         */
084        public boolean login() throws LoginException {
085            loginSucceeded = false;
086            Callback[] callbacks = new Callback[1];
087    
088            callbacks[0] = new CertificateChainCallback();
089            try {
090                handler.handle(callbacks);
091            } catch (IOException ioe) {
092                throw (LoginException) new LoginException().initCause(ioe);
093            } catch (UnsupportedCallbackException uce) {
094                throw (LoginException) new LoginException().initCause(uce);
095            }
096            assert callbacks.length == 1;
097            Certificate[] certificateChain = ((CertificateChainCallback)callbacks[0]).getCertificateChain();
098            if (certificateChain == null || certificateChain.length == 0) {
099                throw new FailedLoginException();
100            }
101            if (!(certificateChain[0] instanceof X509Certificate)) {
102                throw new FailedLoginException();
103            }
104            //TODO actually validate chain
105            principal = ((X509Certificate)certificateChain[0]).getSubjectX500Principal();
106    
107            loginSucceeded = true;
108            return true;
109        }
110    
111        /*
112         * @exception LoginException if login succeeded but commit failed.
113         *
114         * @return true if login succeeded and commit succeeded, or false if login failed but commit succeeded.
115         */
116        public boolean commit() throws LoginException {
117            if(loginSucceeded) {
118                allPrincipals.add(principal);
119                allPrincipals.add(new GeronimoUserPrincipal(principal.getName()));
120                subject.getPrincipals().addAll(allPrincipals);
121            }
122            // Clear out the private state
123            principal = null;
124            
125            return loginSucceeded;
126        }
127    
128        public boolean abort() throws LoginException {
129            if(loginSucceeded) {
130                // Clear out the private state
131                principal = null;
132                allPrincipals.clear();
133            }
134            return loginSucceeded;
135        }
136    
137        public boolean logout() throws LoginException {
138            // Clear out the private state
139            loginSucceeded = false;
140            principal = null;
141            if(!subject.isReadOnly()) {
142                // Remove principals added by this LoginModule
143                subject.getPrincipals().removeAll(allPrincipals);
144            }
145            allPrincipals.clear();
146            return true;
147        }
148    
149    }