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