summaryrefslogtreecommitdiffstats
path: root/src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java')
-rw-r--r--src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java276
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") + ")";
+ }
+}