James Moger
2015-12-10 7b7b0d54b606e5a7d63ea39ec8918968f612d61d
Merge pull request #980 from mrjoel/mrjoel-httpheaders

Refactor authentication for servlet HTTP header handler
1 files added
6 files modified
292 ■■■■■ changed files
src/main/distrib/data/defaults.properties 37 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/ConfigUserService.java 3 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/Constants.java 4 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/auth/AuthenticationProvider.java 53 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/auth/HttpHeaderAuthProvider.java 161 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/AuthenticationManager.java 22 ●●●●● patch | view | raw | blame | history
src/site/setup_authentication.mkd 12 ●●●●● patch | view | raw | blame | history
src/main/distrib/data/defaults.properties
@@ -817,6 +817,7 @@
# Valid providers are:
#
#    htpasswd
#    httpheader
#    ldap
#    pam
#    redmine
@@ -1739,6 +1740,42 @@
# SINCE 1.3.2
realm.htpasswd.userfile = ${baseFolder}/htpasswd
# The name of the HTTP header containing the user name to trust as authenticated
# default: none
#
# WARNING: only use this mechanism if your requests are coming from a trusted
#          and secure source such as a self managed reverse proxy!
#
# RESTART REQUIRED
# SINCE 1.7.2
realm.httpheader.userheader =
# The name of the HTTP header containing the team names of which the user is a member.
# If this is defined, then only groups from the headers will be available, whereas
# if this remains undefined, then local groups will be used.
#
# This setting requires that you have configured realm.httpheader.userheader.
#
# default: none
#
# RESTART REQUIRED
# SINCE 1.7.2
realm.httpheader.teamheader =
# The regular expression pattern used to separate team names in the team header value
# default: ,
#
# This setting requires that you have configured realm.httpheader.teamheader
#
# RESTART REQUIRED
# SINCE 1.7.2
realm.httpheader.teamseparator = ,
# Auto-creates user accounts when successfully authenticated based on HTTP headers.
#
# SINCE 1.7.2
realm.httpheader.autoCreateAccounts = false
# Restrict the Salesforce user to members of this org.
# default: 0 (i.e. do not check the Org ID)
#
src/main/java/com/gitblit/ConfigUserService.java
@@ -890,9 +890,6 @@
                    user.displayName = config.getString(USER, username, DISPLAYNAME);
                    user.emailAddress = config.getString(USER, username, EMAILADDRESS);
                    user.accountType = AccountType.fromString(config.getString(USER, username, ACCOUNTTYPE));
                    if (Constants.EXTERNAL_ACCOUNT.equals(user.password) && user.accountType.isLocal()) {
                        user.accountType = AccountType.EXTERNAL;
                    }
                    user.disabled = config.getBoolean(USER, username, DISABLED, false);
                    user.organizationalUnit = config.getString(USER, username, ORGANIZATIONALUNIT);
                    user.organization = config.getString(USER, username, ORGANIZATION);
src/main/java/com/gitblit/Constants.java
@@ -574,7 +574,7 @@
    }
    public static enum AuthenticationType {
        PUBLIC_KEY, CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER;
        PUBLIC_KEY, CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER, HTTPHEADER;
        public boolean isStandard() {
            return ordinal() <= COOKIE.ordinal();
@@ -582,7 +582,7 @@
    }
    public static enum AccountType {
        LOCAL, EXTERNAL, CONTAINER, LDAP, REDMINE, SALESFORCE, WINDOWS, PAM, HTPASSWD;
        LOCAL, CONTAINER, LDAP, REDMINE, SALESFORCE, WINDOWS, PAM, HTPASSWD, HTTPHEADER;
        public static AccountType fromString(String value) {
            for (AccountType type : AccountType.values()) {
src/main/java/com/gitblit/auth/AuthenticationProvider.java
@@ -18,11 +18,14 @@
import java.io.File;
import java.math.BigInteger;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.Constants.AccountType;
import com.gitblit.Constants.Role;
import com.gitblit.Constants.AuthenticationType;
import com.gitblit.IStoredSettings;
import com.gitblit.manager.IRuntimeManager;
import com.gitblit.manager.IUserManager;
@@ -73,6 +76,8 @@
        return serviceName;
    }
    public abstract AuthenticationType getAuthenticationType();
    protected void setCookie(UserModel user, char [] password) {
        // create a user cookie
        if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {
@@ -116,14 +121,32 @@
    public abstract void stop();
    /**
     * Used to handle requests for requests for pages requiring authentication.
     * This allows authentication to occur based on the contents of the request
     * itself.
     *
     * @param httpRequest
     * @return
     */
    public abstract UserModel authenticate(HttpServletRequest httpRequest);
    /**
     * Used to authentication user/password credentials, both for login form
     * and HTTP Basic authentication processing.
     *
     * @param username
     * @param password
     * @return
     */
    public abstract UserModel authenticate(String username, char[] password);
    public abstract AccountType getAccountType();
    /**
     * Does the user service support changes to credentials?
     * Returns true if the users's credentials can be changed.
     *
     * @return true or false
     * @return true if the authentication provider supports credential changes
     * @since 1.0.0
     */
    public abstract boolean supportsCredentialChanges();
@@ -132,7 +155,7 @@
     * Returns true if the user's display name can be changed.
     *
     * @param user
     * @return true if the user service supports display name changes
     * @return true if the authentication provider supports display name changes
     */
    public abstract boolean supportsDisplayNameChanges();
@@ -140,7 +163,7 @@
     * Returns true if the user's email address can be changed.
     *
     * @param user
     * @return true if the user service supports email address changes
     * @return true if the authentication provider supports email address changes
     */
    public abstract boolean supportsEmailAddressChanges();
@@ -148,7 +171,7 @@
     * Returns true if the user's team memberships can be changed.
     *
     * @param user
     * @return true if the user service supports team membership changes
     * @return true if the authentication provider supports team membership changes
     */
    public abstract boolean supportsTeamMembershipChanges();
@@ -180,6 +203,16 @@
            super(serviceName);
        }
        @Override
        public UserModel authenticate(HttpServletRequest httpRequest) {
            return null;
        }
        @Override
        public AuthenticationType getAuthenticationType() {
            return AuthenticationType.CREDENTIALS;
        }
        @Override
        public void stop() {
@@ -203,6 +236,11 @@
        }
        @Override
        public UserModel authenticate(HttpServletRequest httpRequest) {
            return null;
        }
        @Override
        public UserModel authenticate(String username, char[] password) {
            return null;
        }
@@ -213,6 +251,11 @@
        }
        @Override
        public AuthenticationType getAuthenticationType() {
            return null;
        }
        @Override
        public boolean supportsCredentialChanges() {
            return true;
        }
src/main/java/com/gitblit/auth/HttpHeaderAuthProvider.java
New file
@@ -0,0 +1,161 @@
/*
 * Copyright 2015 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.util.HashSet;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.Constants;
import com.gitblit.Constants.AccountType;
import com.gitblit.Constants.AuthenticationType;
import com.gitblit.Constants.Role;
import com.gitblit.Keys;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.StringUtils;
public class HttpHeaderAuthProvider extends AuthenticationProvider {
    protected final Logger logger = LoggerFactory.getLogger(getClass());
    protected String userHeaderName;
    protected String teamHeaderName;
    protected String teamHeaderSeparator;
    public HttpHeaderAuthProvider() {
        super("httpheader");
    }
    @Override
    public void setup() {
        // Load HTTP header configuration
        userHeaderName = settings.getString(Keys.realm.httpheader.userheader, null);
        teamHeaderName = settings.getString(Keys.realm.httpheader.teamheader, null);
        teamHeaderSeparator = settings.getString(Keys.realm.httpheader.teamseparator, ",");
        if (StringUtils.isEmpty(userHeaderName)) {
            logger.warn("HTTP Header authentication is enabled, but no header is not defined in " + Keys.realm.httpheader.userheader);
        }
    }
    @Override
    public void stop() {}
    @Override
    public UserModel authenticate(HttpServletRequest httpRequest) {
        // Try to authenticate using custom HTTP header if user header is defined
        if (!StringUtils.isEmpty(userHeaderName)) {
            String headerUserName = httpRequest.getHeader(userHeaderName);
            if (!StringUtils.isEmpty(headerUserName) && !userManager.isInternalAccount(headerUserName)) {
                // We have a user, try to load team names as well
                Set<TeamModel> userTeams = new HashSet<>();
                if (!StringUtils.isEmpty(teamHeaderName)) {
                    String headerTeamValue = httpRequest.getHeader(teamHeaderName);
                    if (!StringUtils.isEmpty(headerTeamValue)) {
                        String[] headerTeamNames = headerTeamValue.split(teamHeaderSeparator);
                        for (String teamName : headerTeamNames) {
                            teamName = teamName.trim();
                            if (!StringUtils.isEmpty(teamName)) {
                                TeamModel team = userManager.getTeamModel(teamName);
                                if (null == team) {
                                    // Create teams here so they can marked with the correct AccountType
                                    team = new TeamModel(teamName);
                                    team.accountType = AccountType.HTTPHEADER;
                                    updateTeam(team);
                                }
                                userTeams.add(team);
                            }
                        }
                    }
                }
                UserModel user = userManager.getUserModel(headerUserName);
                if (user != null) {
                    // If team header is provided in request, reset all team memberships, even if resetting to empty set
                    if (!StringUtils.isEmpty(teamHeaderName)) {
                        user.teams.clear();
                        user.teams.addAll(userTeams);
                    }
                    updateUser(user);
                    return user;
                } else if (settings.getBoolean(Keys.realm.httpheader.autoCreateAccounts, false)) {
                    // auto-create user from HTTP header
                    user = new UserModel(headerUserName.toLowerCase());
                    user.displayName = headerUserName;
                    user.password = Constants.EXTERNAL_ACCOUNT;
                    user.accountType = AccountType.HTTPHEADER;
                    user.teams.addAll(userTeams);
                    updateUser(user);
                    return user;
                }
            }
        }
        return null;
    }
    @Override
    public UserModel authenticate(String username, char[] password){
        // Username/password is not supported for HTTP header authentication
        return null;
    }
    @Override
    public AccountType getAccountType() {
        return AccountType.HTTPHEADER;
    }
    @Override
    public AuthenticationType getAuthenticationType() {
        return AuthenticationType.HTTPHEADER;
    }
    @Override
    public boolean supportsCredentialChanges() {
        return false;
    }
    @Override
    public boolean supportsDisplayNameChanges() {
        return false;
    }
    @Override
    public boolean supportsEmailAddressChanges() {
        return false;
    }
    @Override
    public boolean supportsTeamMembershipChanges() {
        return StringUtils.isEmpty(teamHeaderName);
    }
    @Override
    public boolean supportsRoleChanges(UserModel user, Role role) {
        return true;
    }
    @Override
    public boolean supportsRoleChanges(TeamModel team, Role role) {
        return true;
    }
}
src/main/java/com/gitblit/manager/AuthenticationManager.java
@@ -41,6 +41,7 @@
import com.gitblit.auth.AuthenticationProvider;
import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider;
import com.gitblit.auth.HtpasswdAuthProvider;
import com.gitblit.auth.HttpHeaderAuthProvider;
import com.gitblit.auth.LdapAuthProvider;
import com.gitblit.auth.PAMAuthProvider;
import com.gitblit.auth.RedmineAuthProvider;
@@ -92,6 +93,7 @@
        // map of shortcut provider names
        providerNames = new HashMap<String, Class<? extends AuthenticationProvider>>();
        providerNames.put("htpasswd", HtpasswdAuthProvider.class);
        providerNames.put("httpheader", HttpHeaderAuthProvider.class);
        providerNames.put("ldap", LdapAuthProvider.class);
        providerNames.put("pam", PAMAuthProvider.class);
        providerNames.put("redmine", RedmineAuthProvider.class);
@@ -170,7 +172,11 @@
    }
    /**
     * Authenticate a user based on HTTP request parameters.
     * Used to handle authentication for page requests.
     *
     * This allows authentication to occur based on the contents of the request
     * itself. If no configured @{AuthenticationProvider}s authenticate succesffully,
     * a request for login will be shown.
     *
     * Authentication by X509Certificate is tried first and then by cookie.
     *
@@ -185,7 +191,7 @@
    /**
     * Authenticate a user based on HTTP request parameters.
     *
     * Authentication by servlet container principal, X509Certificate, cookie,
     * Authentication by custom HTTP header, servlet container principal, X509Certificate, cookie,
     * and finally BASIC header.
     *
     * @param httpRequest
@@ -319,6 +325,18 @@
                }
            }
        }
        // Check each configured AuthenticationProvider
        for (AuthenticationProvider ap : authenticationProviders) {
            UserModel authedUser = ap.authenticate(httpRequest);
            if (null != authedUser) {
                flagRequest(httpRequest, ap.getAuthenticationType(), authedUser.username);
                logger.debug(MessageFormat.format("{0} authenticated by {1} from {2} for {3}",
                        authedUser.username, ap.getServiceName(), httpRequest.getRemoteAddr(),
                        httpRequest.getPathInfo()));
                return validateAuthentication(authedUser, ap.getAuthenticationType());
            }
        }
        return null;
    }
    
src/site/setup_authentication.mkd
@@ -8,6 +8,7 @@
* Windows authentication
* PAM authentication
* Htpasswd authentication
* HTTP header authentication
* Redmine auhentication
* Salesforce.com authentication
* Servlet container authentication
@@ -101,6 +102,17 @@
    realm.authenticationProviders = htpasswd
    realm.htpasswd.userFile = /path/to/htpasswd
### HTTP Header Authentication
HTTP header authentication allows you to use existing authentication performed by a trusted frontend, such as a reverse proxy. Ensure that when used, gitblit is ONLY availabe via the trusted frontend, otherwise it is vulnerable to a user adding the header explicitly.
By default, no user or team header is defined, which results in all authentication failing this mechanism. The user header can also be defined while leaving the team header undefined, which causes users to be authenticated from the headers, but team memberships to be maintained locally.
    realm.httpheader.userheader = REMOTE_USER
    realm.httpheader.teamheader = X-GitblitExample-GroupNames
    realm.httpheader.teamseparator = ,
    realm.httpheader.autoCreateAccounts = false
### Redmine Authentication
You may authenticate your users against a Redmine installation as long as your Redmine install has properly enabled [API authentication](http://www.redmine.org/projects/redmine/wiki/Rest_Api#Authentication).  This user service only supports user authentication; it does not support team creation based on Redmine groups.  Redmine administrators will also be Gitblit administrators.