diff options
Diffstat (limited to 'src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java')
-rw-r--r-- | src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java | 276 |
1 files changed, 276 insertions, 0 deletions
diff --git a/src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java b/src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java new file mode 100644 index 00000000..559a0fa0 --- /dev/null +++ b/src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java @@ -0,0 +1,276 @@ +/* + * 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; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.StringUtils; + + +/** + * 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<String, String> htUsers = new ConcurrentHashMap<String, String>(); + + 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 + if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) { + user.cookie = StringUtils.getSHA1(user.username + passwd); + } + + // 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") + ")"; + } +} |