/* * Copyright 2013 Florian Zschocke * Copyright 2013 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.auth; import java.io.File; import java.io.FileInputStream; import java.text.MessageFormat; import java.util.Map; import java.util.Scanner; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.digest.Crypt; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.Md5Crypt; import com.gitblit.Constants; import com.gitblit.Constants.AccountType; import com.gitblit.Keys; import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider; import com.gitblit.models.UserModel; /** * Implementation of a user service using an Apache htpasswd file for authentication. * * This user service implement custom authentication using entries in a file created * by the 'htpasswd' program of an Apache web server. All possible output * options of the 'htpasswd' program version 2.2 are supported: * plain text (only on Windows and Netware), * glibc crypt() (not on Windows and NetWare), * Apache MD5 (apr1), * unsalted SHA-1. * * Configuration options: * realm.htpasswd.backingUserService - Specify the backing user service that is used * to keep the user data other than the password. * The default is '${baseFolder}/users.conf'. * realm.htpasswd.userfile - The text file with the htpasswd entries to be used for * authentication. * The default is '${baseFolder}/htpasswd'. * realm.htpasswd.overrideLocalAuthentication - Specify if local accounts are overwritten * when authentication matches for an * external account. * * @author Florian Zschocke * */ public class HtpasswdAuthProvider extends UsernamePasswordAuthenticationProvider { private static final String KEY_HTPASSWD_FILE = Keys.realm.htpasswd.userfile; private static final String DEFAULT_HTPASSWD_FILE = "${baseFolder}/htpasswd"; private static final String KEY_SUPPORT_PLAINTEXT_PWD = "realm.htpasswd.supportPlaintextPasswords"; private boolean supportPlainTextPwd; private File htpasswdFile; private final Map htUsers = new ConcurrentHashMap(); private volatile long lastModified; public HtpasswdAuthProvider() { super("htpasswd"); } /** * Setup the user service. * * The HtpasswdUserService extends the GitblitUserService and is thus * backed by the available user services provided by the GitblitUserService. * In addition the setup tries to read and parse the htpasswd file to be used * for authentication. * * @param settings * @since 0.7.0 */ @Override public void setup() { String os = System.getProperty("os.name").toLowerCase(); if (os.startsWith("windows") || os.startsWith("netware")) { supportPlainTextPwd = true; } else { supportPlainTextPwd = false; } read(); logger.debug("Read " + htUsers.size() + " users from htpasswd file: " + this.htpasswdFile); } @Override public boolean supportsCredentialChanges() { return false; } @Override public boolean supportsDisplayNameChanges() { return true; } @Override public boolean supportsEmailAddressChanges() { return true; } @Override public boolean supportsTeamMembershipChanges() { return true; } /** * Authenticate a user based on a username and password. * * If the account is determined to be a local account, authentication * will be done against the locally stored password. * Otherwise, the configured htpasswd file is read. All current output options * of htpasswd are supported: clear text, crypt(), Apache MD5 and unsalted SHA-1. * * @param username * @param password * @return a user object or null */ @Override public UserModel authenticate(String username, char[] password) { read(); String storedPwd = htUsers.get(username); if (storedPwd != null) { boolean authenticated = false; final String passwd = new String(password); // test Apache MD5 variant encrypted password if (storedPwd.startsWith("$apr1$")) { if (storedPwd.equals(Md5Crypt.apr1Crypt(passwd, storedPwd))) { logger.debug("Apache MD5 encoded password matched for user '" + username + "'"); authenticated = true; } } // test unsalted SHA password else if (storedPwd.startsWith("{SHA}")) { String passwd64 = Base64.encodeBase64String(DigestUtils.sha1(passwd)); if (storedPwd.substring("{SHA}".length()).equals(passwd64)) { logger.debug("Unsalted SHA-1 encoded password matched for user '" + username + "'"); authenticated = true; } } // test libc crypt() encoded password else if (supportCryptPwd() && storedPwd.equals(Crypt.crypt(passwd, storedPwd))) { logger.debug("Libc crypt encoded password matched for user '" + username + "'"); authenticated = true; } // test clear text else if (supportPlaintextPwd() && storedPwd.equals(passwd)){ logger.debug("Clear text password matched for user '" + username + "'"); authenticated = true; } if (authenticated) { logger.debug("Htpasswd authenticated: " + username); UserModel curr = userManager.getUserModel(username); UserModel user; if (curr == null) { // create user object for new authenticated user user = new UserModel(username); } else { user = curr; } // create a user cookie setCookie(user, password); // Set user attributes, hide password from backing user service. user.password = Constants.EXTERNAL_ACCOUNT; user.accountType = getAccountType(); // Push the looked up values to backing file updateUser(user); return user; } } return null; } /** * Get the account type used for this user service. * * @return AccountType.HTPASSWD */ @Override public AccountType getAccountType() { return AccountType.HTPASSWD; } /** * Reads the realm file and rebuilds the in-memory lookup tables. */ protected synchronized void read() { boolean forceReload = false; File file = getFileOrFolder(KEY_HTPASSWD_FILE, DEFAULT_HTPASSWD_FILE); if (!file.equals(htpasswdFile)) { this.htpasswdFile = file; this.htUsers.clear(); forceReload = true; } if (htpasswdFile.exists() && (forceReload || (htpasswdFile.lastModified() != lastModified))) { lastModified = htpasswdFile.lastModified(); htUsers.clear(); Pattern entry = Pattern.compile("^([^:]+):(.+)"); Scanner scanner = null; try { scanner = new Scanner(new FileInputStream(htpasswdFile)); while (scanner.hasNextLine()) { String line = scanner.nextLine().trim(); if (!line.isEmpty() && !line.startsWith("#")) { Matcher m = entry.matcher(line); if (m.matches()) { htUsers.put(m.group(1), m.group(2)); } } } } catch (Exception e) { logger.error(MessageFormat.format("Failed to read {0}", htpasswdFile), e); } finally { if (scanner != null) { scanner.close(); } } } } private boolean supportPlaintextPwd() { return this.settings.getBoolean(KEY_SUPPORT_PLAINTEXT_PWD, supportPlainTextPwd); } private boolean supportCryptPwd() { return !supportPlaintextPwd(); } /* * Method only used for unit tests. Return number of users read from htpasswd file. */ public int getNumberHtpasswdUsers() { return this.htUsers.size(); } @Override public String toString() { return getClass().getSimpleName() + "(" + ((htpasswdFile != null) ? htpasswdFile.getAbsolutePath() : "null") + ")"; } }