| /* |
| * 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<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 |
| 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") + ")"; |
| } |
| } |