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
018package org.apache.activemq.jaas;
019
020import java.io.IOException;
021import java.security.Principal;
022import java.security.cert.X509Certificate;
023import java.util.HashSet;
024import java.util.Iterator;
025import java.util.Map;
026import java.util.Set;
027
028import javax.security.auth.Subject;
029import javax.security.auth.callback.Callback;
030import javax.security.auth.callback.CallbackHandler;
031import javax.security.auth.callback.UnsupportedCallbackException;
032import javax.security.auth.login.FailedLoginException;
033import javax.security.auth.login.LoginException;
034import javax.security.auth.spi.LoginModule;
035
036import org.slf4j.Logger;
037import 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 */
046public 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}