From 3983a6e74d69ed38853c3e89616de3266dcca40d Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 23 Nov 2012 10:17:51 -0500 Subject: [PATCH] X509 certificate authentication based on Kevin Anderson's implementation --- distrib/gitblit.properties | 22 + docs/04_releases.mkd | 5 + src/com/gitblit/AuthenticationFilter.java | 19 + src/com/gitblit/GitBlit.java | 9 + src/com/gitblit/GitFilter.java | 5 + .../gitblit/authority/GitblitAuthority.java | 547 ++++++++++++++++++ src/com/gitblit/utils/HttpUtils.java | 98 ++++ src/com/gitblit/wicket/pages/BasePage.java | 17 +- 8 files changed, 715 insertions(+), 7 deletions(-) create mode 100644 src/com/gitblit/authority/GitblitAuthority.java diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties index 1a5a61b3..1562c513 100644 --- a/distrib/gitblit.properties +++ b/distrib/gitblit.properties @@ -69,6 +69,28 @@ git.submoduleUrlPatterns = .*?://github.com/(.*) # SINCE 0.5.0 git.enableGitServlet = true +# If you want to restrict all git servlet access to those with valid X509 client +# certificates then set this value to true. +# +# SINCE 1.2.0 +git.requiresClientCertificate = false + +# Enforce date checks on client certificates to ensure that they are not being +# used prematurely and that they have not expired. +# +# SINCE 1.2.0 +git.enforceCertificateValidity = true + +# List of OIDs to extract from a client certificate DN to map a certificate to +# an account username. +# +# e.g. git.certificateUsernameOIDs = CN +# e.g. git.certificateUsernameOIDs = FirstName LastName +# +# SPACE-DELIMITED +# SINCE 1.2.0 +git.certificateUsernameOIDs = CN + # Only serve/display bare repositories. # If there are non-bare repositories in git.repositoriesFolder and this setting # is true, they will be excluded from the ui. diff --git a/docs/04_releases.mkd b/docs/04_releases.mkd index d0d24fe7..b8fa5f54 100644 --- a/docs/04_releases.mkd +++ b/docs/04_releases.mkd @@ -47,6 +47,11 @@ In order to fork a repository, the user account must have the *fork* permission **New:** *git.garbageCollectionHour = 0* **New:** *git.defaultGarbageCollectionThreshold = 500k* **New:** *git.defaultGarbageCollectionPeriod = 7 days* +- Added support for X509 client certificate authentication (github/kevinanderson1). +You can require all git servlet access be authenticated by a client certificate. You may also specify the OID fingerprint to use for mapping a certificate to a username. It should be noted that the user account MUST already exist in Gitblit for this authentication mechanism to work; this mechanism can not be used to automatically create user accounts from a certificate. + **New:** *git.requireClientCertificates = false* + **New:** *git.enforceCertificateValidity = true* + **New:** *git.certificateUsernameOIDs = CN* - Added setting to control length of shortened commit ids **New:** *web.shortCommitIdLength=8* - Added simple project pages. A project is a subfolder off the *git.repositoriesFolder*. diff --git a/src/com/gitblit/AuthenticationFilter.java b/src/com/gitblit/AuthenticationFilter.java index 4762c428..64aa4411 100644 --- a/src/com/gitblit/AuthenticationFilter.java +++ b/src/com/gitblit/AuthenticationFilter.java @@ -69,6 +69,15 @@ public abstract class AuthenticationFilter implements Filter { @Override public abstract void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException; + + /** + * Allow the filter to require a client certificate to continue processing. + * + * @return true, if a client certificate is required + */ + protected boolean requiresClientCertificate() { + return false; + } /** * Returns the full relative url of the request. @@ -95,6 +104,16 @@ public abstract class AuthenticationFilter implements Filter { */ protected UserModel getUser(HttpServletRequest httpRequest) { UserModel user = null; + // try request authentication + user = GitBlit.self().authenticate(httpRequest); + if (user != null) { + return user; + } else if (requiresClientCertificate()) { + // http request does not have a valid certificate + // and the filter requires one + return null; + } + // look for client authorization credentials in header final String authorization = httpRequest.getHeader("Authorization"); if (authorization != null && authorization.startsWith(BASIC)) { diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java index b35bf507..612870b6 100644 --- a/src/com/gitblit/GitBlit.java +++ b/src/com/gitblit/GitBlit.java @@ -100,6 +100,7 @@ import com.gitblit.utils.ByteFormat; import com.gitblit.utils.ContainerUtils; import com.gitblit.utils.DeepCopier; import com.gitblit.utils.FederationUtils; +import com.gitblit.utils.HttpUtils; import com.gitblit.utils.JGitUtils; import com.gitblit.utils.JsonUtils; import com.gitblit.utils.MetricUtils; @@ -561,6 +562,14 @@ public class GitBlit implements ServletContextListener { * @return a user object or null */ public UserModel authenticate(HttpServletRequest httpRequest) { + boolean checkValidity = settings.getBoolean(Keys.git.enforceCertificateValidity, true); + String [] oids = getStrings(Keys.git.certificateUsernameOIDs).toArray(new String[0]); + UserModel model = HttpUtils.getUserModelFromCertificate(httpRequest, checkValidity, oids); + if (model != null) { + UserModel user = GitBlit.self().getUserModel(model.username); + logger.info("{0} authenticated by client certificate from {1}", user.username, httpRequest.getRemoteAddr()); + return user; + } return null; } diff --git a/src/com/gitblit/GitFilter.java b/src/com/gitblit/GitFilter.java index 6afdb014..2b769d4b 100644 --- a/src/com/gitblit/GitFilter.java +++ b/src/com/gitblit/GitFilter.java @@ -124,6 +124,11 @@ public class GitFilter extends AccessRestrictionFilter { return true; } + @Override + protected boolean requiresClientCertificate() { + return GitBlit.getBoolean(Keys.git.requiresClientCertificate, false); + } + /** * Determine if the repository requires authentication. * diff --git a/src/com/gitblit/authority/GitblitAuthority.java b/src/com/gitblit/authority/GitblitAuthority.java new file mode 100644 index 00000000..ff48ecf9 --- /dev/null +++ b/src/com/gitblit/authority/GitblitAuthority.java @@ -0,0 +1,547 @@ +/* + * Copyright 2012 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.authority; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.FlowLayout; +import java.awt.Insets; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.activation.DataHandler; +import javax.activation.FileDataSource; +import javax.mail.Message; +import javax.mail.Multipart; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMultipart; +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.RowFilter; +import javax.swing.UIManager; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.TableRowSorter; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; + +import com.gitblit.ConfigUserService; +import com.gitblit.Constants; +import com.gitblit.FileSettings; +import com.gitblit.IStoredSettings; +import com.gitblit.IUserService; +import com.gitblit.Keys; +import com.gitblit.MailExecutor; +import com.gitblit.client.HeaderPanel; +import com.gitblit.client.Translation; +import com.gitblit.models.UserModel; +import com.gitblit.utils.StringUtils; +import com.gitblit.utils.X509Utils; +import com.gitblit.utils.X509Utils.RevocationReason; +import com.gitblit.utils.X509Utils.X509Metadata; + +/** + * Simple GUI tool for administering Gitblit client certificates. + * + * @author James Moger + * + */ +public class GitblitAuthority extends JFrame { + + private static final long serialVersionUID = 1L; + + private final UserCertificateTableModel tableModel; + + private UserCertificatePanel userCertificatePanel; + + private File folder; + + private IStoredSettings gitblitSettings; + + private IUserService userService; + + private String caKeystorePassword = null; + + private JTable table; + + private int defaultDuration; + + private TableRowSorter defaultSorter; + + public static void main(String... args) { + EventQueue.invokeLater(new Runnable() { + public void run() { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + } + GitblitAuthority authority = new GitblitAuthority(); + authority.initialize(); + authority.setLocationRelativeTo(null); + authority.setVisible(true); + } + }); + } + + public GitblitAuthority() { + super(); + tableModel = new UserCertificateTableModel(); + defaultSorter = new TableRowSorter(tableModel); + } + + public void initialize() { + setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage()); + setTitle("Gitblit PKI Authority v" + Constants.VERSION + " (" + Constants.VERSION_DATE + ")"); + setContentPane(getUI()); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent event) { + saveSizeAndPosition(); + } + + @Override + public void windowOpened(WindowEvent event) { + } + }); + + setSizeAndPosition(); + + File folder = new File(System.getProperty("user.dir")); + load(folder); + } + + private void setSizeAndPosition() { + String sz = null; + String pos = null; + try { + StoredConfig config = getConfig(); + sz = config.getString("ui", null, "size"); + pos = config.getString("ui", null, "position"); + defaultDuration = config.getInt("new", "duration", 365); + } catch (Throwable t) { + t.printStackTrace(); + } + + // try to restore saved window size + if (StringUtils.isEmpty(sz)) { + setSize(850, 500); + } else { + String[] chunks = sz.split("x"); + int width = Integer.parseInt(chunks[0]); + int height = Integer.parseInt(chunks[1]); + setSize(width, height); + } + + // try to restore saved window position + if (StringUtils.isEmpty(pos)) { + setLocationRelativeTo(null); + } else { + String[] chunks = pos.split(","); + int x = Integer.parseInt(chunks[0]); + int y = Integer.parseInt(chunks[1]); + setLocation(x, y); + } + } + + private void saveSizeAndPosition() { + try { + // save window size and position + StoredConfig config = getConfig(); + Dimension sz = GitblitAuthority.this.getSize(); + config.setString("ui", null, "size", + MessageFormat.format("{0,number,0}x{1,number,0}", sz.width, sz.height)); + Point pos = GitblitAuthority.this.getLocationOnScreen(); + config.setString("ui", null, "position", + MessageFormat.format("{0,number,0},{1,number,0}", pos.x, pos.y)); + config.save(); + } catch (Throwable t) { + Utils.showException(GitblitAuthority.this, t); + } + } + + private StoredConfig getConfig() throws IOException, ConfigInvalidException { + File configFile = new File(System.getProperty("user.dir"), X509Utils.CA_CONFIG); + FileBasedConfig config = new FileBasedConfig(configFile, FS.detect()); + config.load(); + return config; + } + + private IUserService loadUsers(File folder) { + File file = new File(folder, "gitblit.properties"); + if (!file.exists()) { + return null; + } + gitblitSettings = new FileSettings(file.getAbsolutePath()); + caKeystorePassword = gitblitSettings.getString(Keys.server.storePassword, null); + String us = gitblitSettings.getString(Keys.realm.userService, "users.conf"); + String ext = us.substring(us.lastIndexOf(".") + 1).toLowerCase(); + IUserService service = null; + if (!ext.equals("conf") && !ext.equals("properties")) { + if (us.equals("com.gitblit.LdapUserService")) { + us = gitblitSettings.getString(Keys.realm.ldap.backingUserService, "users.conf"); + } else if (us.equals("com.gitblit.LdapUserService")) { + us = gitblitSettings.getString(Keys.realm.redmine.backingUserService, "users.conf"); + } + } + + if (us.endsWith(".conf")) { + service = new ConfigUserService(new File(us)); + } else { + throw new RuntimeException("Unsupported user service: " + us); + } + + service = new ConfigUserService(new File(us)); + return service; + } + + private void load(File folder) { + this.folder = folder; + this.userService = loadUsers(folder); + if (userService != null) { + // build empty certificate model for all users + Map map = new HashMap(); + for (String user : userService.getAllUsernames()) { + UserModel model = userService.getUserModel(user); + UserCertificateModel ucm = new UserCertificateModel(model); + map.put(user, ucm); + } + File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG); + FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect()); + if (certificatesConfigFile.exists()) { + try { + config.load(); + // replace user certificate model with actual data + List list = UserCertificateConfig.KEY.parse(config).list; + for (UserCertificateModel ucm : list) { + ucm.user = userService.getUserModel(ucm.user.username); + map.put(ucm.user.username, ucm); + } + } catch (IOException e) { + e.printStackTrace(); + } catch (ConfigInvalidException e) { + e.printStackTrace(); + } + } + + tableModel.list = new ArrayList(map.values()); + Collections.sort(tableModel.list); + tableModel.fireTableDataChanged(); + } + } + + private List findCerts(File folder, String username) { + List list = new ArrayList(); + File userFolder = new File(folder, X509Utils.CERTS + File.separator + username); + if (!userFolder.exists()) { + return list; + } + File [] certs = userFolder.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.toLowerCase().endsWith(".cer") || name.toLowerCase().endsWith(".crt"); + } + }); + try { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + for (File cert : certs) { + BufferedInputStream is = new BufferedInputStream(new FileInputStream(cert)); + X509Certificate x509 = (X509Certificate) factory.generateCertificate(is); + is.close(); + list.add(x509); + } + } catch (Exception e) { + Utils.showException(GitblitAuthority.this, e); + } + return list; + } + + private Container getUI() { + userCertificatePanel = new UserCertificatePanel(this) { + + private static final long serialVersionUID = 1L; + @Override + public Insets getInsets() { + return Utils.INSETS; + } + + @Override + public Date getDefaultExpiration() { + Calendar c = Calendar.getInstance(); + c.add(Calendar.DATE, defaultDuration); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + return c.getTime(); + } + + @Override + public void saveUser(String username, UserCertificateModel ucm) { + userService.updateUserModel(username, ucm.user); + } + + @Override + public void newCertificate(UserCertificateModel ucm, X509Metadata metadata, boolean sendEmail) { + Date notAfter = metadata.notAfter; + metadata.serverHostname = gitblitSettings.getString(Keys.web.siteName, "localhost"); + UserModel user = ucm.user; + + // set default values from config file + File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG); + FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect()); + if (certificatesConfigFile.exists()) { + try { + config.load(); + } catch (Exception e) { + Utils.showException(GitblitAuthority.this, e); + } + NewCertificateConfig certificateConfig = NewCertificateConfig.KEY.parse(config); + certificateConfig.update(metadata); + } + + // restore expiration date + metadata.notAfter = notAfter; + + // set user's specified OID values + if (!StringUtils.isEmpty(user.organizationalUnit)) { + metadata.oids.put("OU", user.organizationalUnit); + } + if (!StringUtils.isEmpty(user.organization)) { + metadata.oids.put("O", user.organization); + } + if (!StringUtils.isEmpty(user.locality)) { + metadata.oids.put("L", user.locality); + } + if (!StringUtils.isEmpty(user.stateProvince)) { + metadata.oids.put("ST", user.stateProvince); + } + if (!StringUtils.isEmpty(user.countryCode)) { + metadata.oids.put("C", user.countryCode); + } + + File caKeystoreFile = new File(folder, X509Utils.CA_KEY_STORE); + File zip = X509Utils.newClientBundle(metadata, caKeystoreFile, caKeystorePassword); + + // save latest expiration date + if (ucm.expires == null || metadata.notAfter.after(ucm.expires)) { + ucm.expires = metadata.notAfter; + } + ucm.update(config); + try { + config.save(); + } catch (Exception e) { + Utils.showException(GitblitAuthority.this, e); + } + + // refresh user + ucm.certs = null; + int modelIndex = table.convertRowIndexToModel(table.getSelectedRow()); + tableModel.fireTableDataChanged(); + table.getSelectionModel().setSelectionInterval(modelIndex, modelIndex); + + if (sendEmail) { + // send email + try { + MailExecutor mail = new MailExecutor(gitblitSettings); + if (mail.isReady()) { + Message message = mail.createMessage(user.emailAddress); + message.setSubject("Your Gitblit client certificate for " + metadata.serverHostname); + + // body of email + String body = X509Utils.processTemplate(new File(caKeystoreFile.getParentFile(), "mail.tmpl"), metadata); + if (StringUtils.isEmpty(body)) { + body = MessageFormat.format("Hi {0}\n\nHere is your client certificate bundle.\nInside the zip file are installation instructions.", user.getDisplayName()); + } + Multipart mp = new MimeMultipart(); + MimeBodyPart messagePart = new MimeBodyPart(); + messagePart.setText(body); + mp.addBodyPart(messagePart); + + // attach zip + MimeBodyPart filePart = new MimeBodyPart(); + FileDataSource fds = new FileDataSource(zip); + filePart.setDataHandler(new DataHandler(fds)); + filePart.setFileName(fds.getName()); + mp.addBodyPart(filePart); + + message.setContent(mp); + + mail.sendNow(message); + } else { + JOptionPane.showMessageDialog(GitblitAuthority.this, "Sorry, the mail server settings are not configured properly.\nCan not send email.", Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE); + } + } catch (Exception e) { + Utils.showException(GitblitAuthority.this, e); + } + } + } + + @Override + public void revoke(UserCertificateModel ucm, X509Certificate cert, RevocationReason reason) { + File caRevocationList = new File(folder, X509Utils.CA_REVOCATION_LIST); + File caKeystoreFile = new File(folder, X509Utils.CA_KEY_STORE); + if (X509Utils.revoke(cert, reason, caRevocationList, caKeystoreFile, caKeystorePassword)) { + File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG); + FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect()); + if (certificatesConfigFile.exists()) { + try { + config.load(); + } catch (Exception e) { + Utils.showException(GitblitAuthority.this, e); + } + } + // add serial to revoked list + ucm.revoke(cert.getSerialNumber(), reason); + ucm.update(config); + try { + config.save(); + } catch (Exception e) { + Utils.showException(GitblitAuthority.this, e); + } + + // refresh user + ucm.certs = null; + int modelIndex = table.convertRowIndexToModel(table.getSelectedRow()); + tableModel.fireTableDataChanged(); + table.getSelectionModel().setSelectionInterval(modelIndex, modelIndex); + } + } + }; + + table = Utils.newTable(tableModel, Utils.DATE_FORMAT); + table.setRowSorter(defaultSorter); + table.setDefaultRenderer(CertificateStatus.class, new CertificateStatusRenderer()); + table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getValueIsAdjusting()) { + return; + } + int row = table.getSelectedRow(); + if (row < 0) { + return; + } + int modelIndex = table.convertRowIndexToModel(row); + UserCertificateModel ucm = tableModel.get(modelIndex); + if (ucm.certs == null) { + ucm.certs = findCerts(folder, ucm.user.username); + } + userCertificatePanel.setUserCertificateModel(ucm); + } + }); + + JPanel usersPanel = new JPanel(new BorderLayout()) { + + private static final long serialVersionUID = 1L; + + @Override + public Insets getInsets() { + return Utils.INSETS; + } + }; + usersPanel.add(new HeaderPanel(Translation.get("gb.users"), "users_16x16.png"), BorderLayout.NORTH); + usersPanel.add(new JScrollPane(table), BorderLayout.CENTER); + usersPanel.setMinimumSize(new Dimension(400, 10)); + + final JTextField filterTextfield = new JTextField(20); + filterTextfield.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + filterUsers(filterTextfield.getText()); + } + }); + filterTextfield.addKeyListener(new KeyAdapter() { + public void keyReleased(KeyEvent e) { + filterUsers(filterTextfield.getText()); + } + }); + + JPanel userControls = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 5)); + userControls.add(new JLabel(Translation.get("gb.filter"))); + userControls.add(filterTextfield); + + JPanel leftPanel = new JPanel(new BorderLayout()); + leftPanel.add(userControls, BorderLayout.NORTH); + leftPanel.add(usersPanel, BorderLayout.CENTER); + + userCertificatePanel.setMinimumSize(new Dimension(375, 10)); + + JPanel root = new JPanel(new BorderLayout()) { + private static final long serialVersionUID = 1L; + public Insets getInsets() { + return Utils.INSETS; + } + }; + JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, userCertificatePanel); + splitPane.setDividerLocation(1d); + root.add(splitPane); + return root; + } + + private void filterUsers(final String fragment) { + if (StringUtils.isEmpty(fragment)) { + table.setRowSorter(defaultSorter); + return; + } + RowFilter containsFilter = new RowFilter() { + public boolean include(Entry entry) { + for (int i = entry.getValueCount() - 1; i >= 0; i--) { + if (entry.getStringValue(i).toLowerCase().contains(fragment.toLowerCase())) { + return true; + } + } + return false; + } + }; + TableRowSorter sorter = new TableRowSorter( + tableModel); + sorter.setRowFilter(containsFilter); + table.setRowSorter(sorter); + } +} diff --git a/src/com/gitblit/utils/HttpUtils.java b/src/com/gitblit/utils/HttpUtils.java index ad7d58c1..68a35066 100644 --- a/src/com/gitblit/utils/HttpUtils.java +++ b/src/com/gitblit/utils/HttpUtils.java @@ -15,8 +15,20 @@ */ package com.gitblit.utils; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.text.MessageFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + import javax.servlet.http.HttpServletRequest; +import org.slf4j.LoggerFactory; + +import com.gitblit.models.UserModel; + /** * Collection of utility methods for http requests. * @@ -92,4 +104,90 @@ public class HttpUtils { sb.append(context); return sb.toString(); } + + /** + * Returns a user model object built from attributes in the SSL certificate. + * This model is not retrieved from the user service. + * + * @param httpRequest + * @param checkValidity ensure certificate can be used now + * @param usernameOIDs if unspecified, CN is used as the username + * @return a UserModel, if a valid certificate is in the request, null otherwise + */ + public static UserModel getUserModelFromCertificate(HttpServletRequest httpRequest, boolean checkValidity, String... usernameOIDs) { + if (httpRequest.getAttribute("javax.servlet.request.X509Certificate") != null) { + X509Certificate[] certChain = (X509Certificate[]) httpRequest + .getAttribute("javax.servlet.request.X509Certificate"); + if (certChain != null) { + X509Certificate cert = certChain[0]; + // ensure certificate is valid + if (checkValidity) { + try { + cert.checkValidity(new Date()); + } catch (CertificateNotYetValidException e) { + LoggerFactory.getLogger(HttpUtils.class).info(MessageFormat.format("X509 certificate {0} is not yet valid", cert.getSubjectDN().getName())); + return null; + } catch (CertificateExpiredException e) { + LoggerFactory.getLogger(HttpUtils.class).info(MessageFormat.format("X509 certificate {0} has expired", cert.getSubjectDN().getName())); + return null; + } + } + return getUserModelFromCertificate(cert, usernameOIDs); + } + } + return null; + } + + /** + * Creates a UserModel from a certificate + * @param cert + * @param usernameOids if unspecified CN is used as the username + * @return + */ + public static UserModel getUserModelFromCertificate(X509Certificate cert, String... usernameOIDs) { + UserModel user = new UserModel(null); + user.isAuthenticated = false; + + // manually split DN into OID components + // this is instead of parsing with LdapName which: + // (1) I don't trust the order of values + // (2) it filters out values like EMAILADDRESS + String dn = cert.getSubjectDN().getName(); + Map oids = new HashMap(); + for (String kvp : dn.split(",")) { + String [] val = kvp.trim().split("="); + String oid = val[0].toUpperCase().trim(); + String data = val[1].trim(); + oids.put(oid, data); + } + + if (usernameOIDs == null || usernameOIDs.length == 0) { + // use default usename<->CN mapping + usernameOIDs = new String [] { "CN" }; + } + + // determine username from OID fingerprint + StringBuilder an = new StringBuilder(); + for (String oid : usernameOIDs) { + String val = getOIDValue(oid.toUpperCase(), oids); + if (val != null) { + an.append(val).append(' '); + } + } + user.username = an.toString().trim(); + + // extract email address, if available + user.emailAddress = getOIDValue("E", oids); + if (user.emailAddress == null) { + user.emailAddress = getOIDValue("EMAILADDRESS", oids); + } + return user; + } + + private static String getOIDValue(String oid, Map oids) { + if (oids.containsKey(oid)) { + return oids.get(oid); + } + return null; + } } diff --git a/src/com/gitblit/wicket/pages/BasePage.java b/src/com/gitblit/wicket/pages/BasePage.java index ceeb9120..05640ad0 100644 --- a/src/com/gitblit/wicket/pages/BasePage.java +++ b/src/com/gitblit/wicket/pages/BasePage.java @@ -131,13 +131,16 @@ public abstract class BasePage extends WebPage { } private void login() { - Cookie[] cookies = ((WebRequest) getRequestCycle().getRequest()).getCookies(); - UserModel user = null; - if (GitBlit.self().allowCookieAuthentication() && cookies != null && cookies.length > 0) { - // Grab cookie from Browser Session - user = GitBlit.self().authenticate(cookies); - } else { - user = GitBlit.self().authenticate(((WebRequest) getRequestCycle().getRequest()).getHttpServletRequest()); + // try to authenticate by servlet request + UserModel user = GitBlit.self().authenticate(((WebRequest) getRequestCycle().getRequest()).getHttpServletRequest()); + + if (user == null) { + // try to authenticate by cookie + Cookie[] cookies = ((WebRequest) getRequestCycle().getRequest()).getCookies(); + if (GitBlit.self().allowCookieAuthentication() && cookies != null && cookies.length > 0) { + // Grab cookie from Browser Session + user = GitBlit.self().authenticate(cookies); + } } // Login the user -- 2.39.5