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