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.activemq.jaas;
019    
020    import java.io.IOException;
021    import java.security.Principal;
022    import java.security.cert.X509Certificate;
023    import java.util.HashSet;
024    import java.util.Iterator;
025    import java.util.Map;
026    import java.util.Set;
027    
028    import javax.security.auth.Subject;
029    import javax.security.auth.callback.Callback;
030    import javax.security.auth.callback.CallbackHandler;
031    import javax.security.auth.callback.UnsupportedCallbackException;
032    import javax.security.auth.login.FailedLoginException;
033    import javax.security.auth.login.LoginException;
034    import javax.security.auth.spi.LoginModule;
035    
036    import org.slf4j.Logger;
037    import org.slf4j.LoggerFactory;
038    
039    /**
040     * A LoginModule that allows for authentication based on SSL certificates.
041     * Allows for subclasses to define methods used to verify user certificates and
042     * find user groups. Uses CertificateCallbacks to retrieve certificates.
043     * 
044     * @author sepandm@gmail.com (Sepand)
045     */
046    public abstract class CertificateLoginModule implements LoginModule {
047    
048        private static final Logger LOG = LoggerFactory.getLogger(CertificateLoginModule.class);
049    
050        private CallbackHandler callbackHandler;
051        private Subject subject;
052    
053        private X509Certificate certificates[];
054        private String username;
055        private Set<String> groups;
056        private Set<Principal> principals = new HashSet<Principal>();
057        private boolean debug;
058    
059        /**
060         * Overriding to allow for proper initialization. Standard JAAS.
061         */
062        @Override
063        public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
064            this.subject = subject;
065            this.callbackHandler = callbackHandler;
066    
067            debug = "true".equalsIgnoreCase((String)options.get("debug"));
068    
069            if (debug) {
070                LOG.debug("Initialized debug");
071            }
072        }
073    
074        /**
075         * Overriding to allow for certificate-based login. Standard JAAS.
076         */
077        @Override
078        public boolean login() throws LoginException {
079            Callback[] callbacks = new Callback[1];
080    
081            callbacks[0] = new CertificateCallback();
082            try {
083                callbackHandler.handle(callbacks);
084            } catch (IOException ioe) {
085                throw new LoginException(ioe.getMessage());
086            } catch (UnsupportedCallbackException uce) {
087                throw new LoginException(uce.getMessage() + " Unable to obtain client certificates.");
088            }
089            certificates = ((CertificateCallback)callbacks[0]).getCertificates();
090    
091            username = getUserNameForCertificates(certificates);
092            if (username == null) {
093                throw new FailedLoginException("No user for client certificate: " + getDistinguishedName(certificates));
094            }
095    
096            groups = getUserGroups(username);
097    
098            if (debug) {
099                LOG.debug("Certificate for user: " + username);
100            }
101            return true;
102        }
103    
104        /**
105         * Overriding to complete login process. Standard JAAS.
106         */
107        @Override
108        public boolean commit() throws LoginException {
109            principals.add(new UserPrincipal(username));
110    
111            for (String group : groups) {
112                 principals.add(new GroupPrincipal(group));
113            }
114    
115            subject.getPrincipals().addAll(principals);
116    
117            clear();
118    
119            if (debug) {
120                LOG.debug("commit");
121            }
122            return true;
123        }
124    
125        /**
126         * Standard JAAS override.
127         */
128        @Override
129        public boolean abort() throws LoginException {
130            clear();
131    
132            if (debug) {
133                LOG.debug("abort");
134            }
135            return true;
136        }
137    
138        /**
139         * Standard JAAS override.
140         */
141        @Override
142        public boolean logout() {
143            subject.getPrincipals().removeAll(principals);
144            principals.clear();
145    
146            if (debug) {
147                LOG.debug("logout");
148            }
149            return true;
150        }
151    
152        /**
153         * Helper method.
154         */
155        private void clear() {
156            groups.clear();
157            certificates = null;
158        }
159    
160        /**
161         * Should return a unique name corresponding to the certificates given. The
162         * name returned will be used to look up access levels as well as group
163         * associations.
164         * 
165         * @param certs The distinguished name.
166         * @return The unique name if the certificate is recognized, null otherwise.
167         */
168        protected abstract String getUserNameForCertificates(final X509Certificate[] certs) throws LoginException;
169    
170        /**
171         * Should return a set of the groups this user belongs to. The groups
172         * returned will be added to the user's credentials.
173         * 
174         * @param username The username of the client. This is the same name that
175         *                getUserNameForDn returned for the user's DN.
176         * @return A Set of the names of the groups this user belongs to.
177         */
178        protected abstract Set<String> getUserGroups(final String username) throws LoginException;
179    
180        protected String getDistinguishedName(final X509Certificate[] certs) {
181            if (certs != null && certs.length > 0 && certs[0] != null) {
182                return certs[0].getSubjectDN().getName();
183            } else {
184                return null;
185            }
186        }
187    
188    }