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 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    public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
063        this.subject = subject;
064        this.callbackHandler = callbackHandler;
065
066        debug = "true".equalsIgnoreCase((String)options.get("debug"));
067
068        if (debug) {
069            LOG.debug("Initialized debug");
070        }
071    }
072
073    /**
074     * Overriding to allow for certificate-based login. Standard JAAS.
075     */
076    public boolean login() throws LoginException {
077        Callback[] callbacks = new Callback[1];
078
079        callbacks[0] = new CertificateCallback();
080        try {
081            callbackHandler.handle(callbacks);
082        } catch (IOException ioe) {
083            throw new LoginException(ioe.getMessage());
084        } catch (UnsupportedCallbackException uce) {
085            throw new LoginException(uce.getMessage() + " Unable to obtain client certificates.");
086        }
087        certificates = ((CertificateCallback)callbacks[0]).getCertificates();
088
089        username = getUserNameForCertificates(certificates);
090        if (username == null) {
091            throw new FailedLoginException("No user for client certificate: " + getDistinguishedName(certificates));
092        }
093
094        groups = getUserGroups(username);
095
096        if (debug) {
097            LOG.debug("Certificate for user: " + username);
098        }
099        return true;
100    }
101
102    /**
103     * Overriding to complete login process. Standard JAAS.
104     */
105    public boolean commit() throws LoginException {
106        principals.add(new UserPrincipal(username));
107
108        String currentGroup = null;
109        for (Iterator iter = groups.iterator(); iter.hasNext();) {
110            currentGroup = (String)iter.next();
111            principals.add(new GroupPrincipal(currentGroup));
112        }
113
114        subject.getPrincipals().addAll(principals);
115
116        clear();
117
118        if (debug) {
119            LOG.debug("commit");
120        }
121        return true;
122    }
123
124    /**
125     * Standard JAAS override.
126     */
127    public boolean abort() throws LoginException {
128        clear();
129
130        if (debug) {
131            LOG.debug("abort");
132        }
133        return true;
134    }
135
136    /**
137     * Standard JAAS override.
138     */
139    public boolean logout() {
140        subject.getPrincipals().removeAll(principals);
141        principals.clear();
142
143        if (debug) {
144            LOG.debug("logout");
145        }
146        return true;
147    }
148
149    /**
150     * Helper method.
151     */
152    private void clear() {
153        groups.clear();
154        certificates = null;
155    }
156
157    /**
158     * Should return a unique name corresponding to the certificates given. The
159     * name returned will be used to look up access levels as well as group
160     * associations.
161     * 
162     * @param certs The distinguished name.
163     * @return The unique name if the certificate is recognized, null otherwise.
164     */
165    protected abstract String getUserNameForCertificates(final X509Certificate[] certs) throws LoginException;
166
167    /**
168     * Should return a set of the groups this user belongs to. The groups
169     * returned will be added to the user's credentials.
170     * 
171     * @param username The username of the client. This is the same name that
172     *                getUserNameForDn returned for the user's DN.
173     * @return A Set of the names of the groups this user belongs to.
174     */
175    protected abstract Set getUserGroups(final String username) throws LoginException;
176
177    protected String getDistinguishedName(final X509Certificate[] certs) {
178        if (certs != null && certs.length > 0 && certs[0] != null) {
179            return certs[0].getSubjectDN().getName();
180        } else {
181            return null;
182        }
183    }
184
185}