/* * 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.manager; import java.nio.charset.Charset; import java.security.Principal; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.wicket.RequestCycle; 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.IStoredSettings; import com.gitblit.Keys; import com.gitblit.auth.AuthenticationProvider; import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider; import com.gitblit.auth.HtpasswdAuthProvider; import com.gitblit.auth.LdapAuthProvider; import com.gitblit.auth.PAMAuthProvider; import com.gitblit.auth.RedmineAuthProvider; import com.gitblit.auth.SalesforceAuthProvider; import com.gitblit.auth.WindowsAuthProvider; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; import com.gitblit.transport.ssh.SshSession; import com.gitblit.utils.Base64; import com.gitblit.utils.HttpUtils; import com.gitblit.utils.StringUtils; import com.gitblit.utils.X509Utils.X509Metadata; import com.gitblit.wicket.GitBlitWebSession; /** * The authentication manager handles user login & logout. * * @author James Moger * */ public class AuthenticationManager implements IAuthenticationManager { private final Logger logger = LoggerFactory.getLogger(getClass()); private final IStoredSettings settings; private final IRuntimeManager runtimeManager; private final IUserManager userManager; private final List authenticationProviders; private final Map> providerNames; private final Map legacyRedirects; public AuthenticationManager( IRuntimeManager runtimeManager, IUserManager userManager) { this.settings = runtimeManager.getSettings(); this.runtimeManager = runtimeManager; this.userManager = userManager; this.authenticationProviders = new ArrayList(); // map of shortcut provider names providerNames = new HashMap>(); providerNames.put("htpasswd", HtpasswdAuthProvider.class); providerNames.put("ldap", LdapAuthProvider.class); providerNames.put("pam", PAMAuthProvider.class); providerNames.put("redmine", RedmineAuthProvider.class); providerNames.put("salesforce", SalesforceAuthProvider.class); providerNames.put("windows", WindowsAuthProvider.class); // map of legacy external user services legacyRedirects = new HashMap(); legacyRedirects.put("com.gitblit.HtpasswdUserService", "htpasswd"); legacyRedirects.put("com.gitblit.LdapUserService", "ldap"); legacyRedirects.put("com.gitblit.PAMUserService", "pam"); legacyRedirects.put("com.gitblit.RedmineUserService", "redmine"); legacyRedirects.put("com.gitblit.SalesforceUserService", "salesforce"); legacyRedirects.put("com.gitblit.WindowsUserService", "windows"); } @Override public AuthenticationManager start() { // automatically adjust legacy configurations String realm = settings.getString(Keys.realm.userService, "${baseFolder}/users.conf"); if (legacyRedirects.containsKey(realm)) { logger.warn(""); logger.warn(Constants.BORDER2); logger.warn(" IUserService '{}' is obsolete!", realm); logger.warn(" Please set '{}={}'", "realm.authenticationProviders", legacyRedirects.get(realm)); logger.warn(Constants.BORDER2); logger.warn(""); // conditionally override specified authentication providers if (StringUtils.isEmpty(settings.getString(Keys.realm.authenticationProviders, null))) { settings.overrideSetting(Keys.realm.authenticationProviders, legacyRedirects.get(realm)); } } // instantiate and setup specified authentication providers List providers = settings.getStrings(Keys.realm.authenticationProviders); if (providers.isEmpty()) { logger.info("External authentication disabled."); } else { for (String provider : providers) { try { Class authClass; if (providerNames.containsKey(provider)) { // map the name -> class authClass = providerNames.get(provider); } else { // reflective lookup authClass = Class.forName(provider); } logger.info("setting up {}", authClass.getName()); AuthenticationProvider authImpl = (AuthenticationProvider) authClass.newInstance(); authImpl.setup(runtimeManager, userManager); authenticationProviders.add(authImpl); } catch (Exception e) { logger.error("", e); } } } return this; } @Override public AuthenticationManager stop() { for (AuthenticationProvider provider : authenticationProviders) { try { provider.stop(); } catch (Exception e) { logger.error("Failed to stop " + provider.getClass().getSimpleName(), e); } } return this; } public void addAuthenticationProvider(AuthenticationProvider prov) { authenticationProviders.add(prov); } /** * Authenticate a user based on HTTP request parameters. * * Authentication by X509Certificate is tried first and then by cookie. * * @param httpRequest * @return a user object or null */ @Override public UserModel authenticate(HttpServletRequest httpRequest) { return authenticate(httpRequest, false); } /** * Authenticate a user based on HTTP request parameters. * * Authentication by servlet container principal, X509Certificate, cookie, * and finally BASIC header. * * @param httpRequest * @param requiresCertificate * @return a user object or null */ @Override public UserModel authenticate(HttpServletRequest httpRequest, boolean requiresCertificate) { // try to authenticate by servlet container principal if (!requiresCertificate) { Principal principal = httpRequest.getUserPrincipal(); if (principal != null) { String username = principal.getName(); if (!StringUtils.isEmpty(username)) { boolean internalAccount = userManager.isInternalAccount(username); UserModel user = userManager.getUserModel(username); if (user != null) { // existing user flagWicketSession(AuthenticationType.CONTAINER); logger.debug(MessageFormat.format("{0} authenticated by servlet container principal from {1}", user.username, httpRequest.getRemoteAddr())); return validateAuthentication(user, AuthenticationType.CONTAINER); } else if (settings.getBoolean(Keys.realm.container.autoCreateAccounts, false) && !internalAccount) { // auto-create user from an authenticated container principal user = new UserModel(username.toLowerCase()); user.displayName = username; user.password = Constants.EXTERNAL_ACCOUNT; user.accountType = AccountType.CONTAINER; userManager.updateUserModel(user); flagWicketSession(AuthenticationType.CONTAINER); logger.debug(MessageFormat.format("{0} authenticated and created by servlet container principal from {1}", user.username, httpRequest.getRemoteAddr())); return validateAuthentication(user, AuthenticationType.CONTAINER); } else if (!internalAccount) { logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted servlet container authentication from {1}", principal.getName(), httpRequest.getRemoteAddr())); } } } } // try to authenticate by certificate boolean checkValidity = settings.getBoolean(Keys.git.enforceCertificateValidity, true); String [] oids = settings.getStrings(Keys.git.certificateUsernameOIDs).toArray(new String[0]); UserModel model = HttpUtils.getUserModelFromCertificate(httpRequest, checkValidity, oids); if (model != null) { // grab real user model and preserve certificate serial number UserModel user = userManager.getUserModel(model.username); X509Metadata metadata = HttpUtils.getCertificateMetadata(httpRequest); if (user != null) { flagWicketSession(AuthenticationType.CERTIFICATE); logger.debug(MessageFormat.format("{0} authenticated by client certificate {1} from {2}", user.username, metadata.serialNumber, httpRequest.getRemoteAddr())); return validateAuthentication(user, AuthenticationType.CERTIFICATE); } else { logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted client certificate ({1}) authentication from {2}", model.username, metadata.serialNumber, httpRequest.getRemoteAddr())); } } if (requiresCertificate) { // caller requires client certificate authentication (e.g. git servlet) return null; } UserModel user = null; // try to authenticate by cookie String cookie = getCookie(httpRequest); if (!StringUtils.isEmpty(cookie)) { user = userManager.getUserModel(cookie.toCharArray()); if (user != null) { flagWicketSession(AuthenticationType.COOKIE); logger.debug(MessageFormat.format("{0} authenticated by cookie from {1}", user.username, httpRequest.getRemoteAddr())); return validateAuthentication(user, AuthenticationType.COOKIE); } } // try to authenticate by BASIC final String authorization = httpRequest.getHeader("Authorization"); if (authorization != null && authorization.startsWith("Basic")) { // Authorization: Basic base64credentials String base64Credentials = authorization.substring("Basic".length()).trim(); String credentials = new String(Base64.decode(base64Credentials), Charset.forName("UTF-8")); // credentials = username:password final String[] values = credentials.split(":", 2); if (values.length == 2) { String username = values[0]; char[] password = values[1].toCharArray(); user = authenticate(username, password); if (user != null) { flagWicketSession(AuthenticationType.CREDENTIALS); logger.debug(MessageFormat.format("{0} authenticated by BASIC request header from {1}", user.username, httpRequest.getRemoteAddr())); return validateAuthentication(user, AuthenticationType.CREDENTIALS); } else { logger.warn(MessageFormat.format("Failed login attempt for {0}, invalid credentials from {1}", username, httpRequest.getRemoteAddr())); } } } return null; } /** * Authenticate a user based on SSH session. * * @param SshSession * @return a user object or null */ @Override public UserModel authenticate(SshSession sshSession) { String username = sshSession.getRemoteUser(); if (username != null) { if (!StringUtils.isEmpty(username)) { UserModel user = userManager.getUserModel(username); if (user != null) { // existing user logger.debug(MessageFormat.format("{0} authenticated by servlet container principal from {1}", user.username, sshSession.getRemoteAddress())); return validateAuthentication(user, AuthenticationType.SSH); } logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted ssh authentication from {1}", username, sshSession.getRemoteAddress())); } } else { logger.warn("Empty user in SSH session"); } return null; } /** * This method allows the authentication manager to reject authentication * attempts. It is called after the username/secret have been verified to * ensure that the authentication technique has been logged. * * @param user * @return */ protected UserModel validateAuthentication(UserModel user, AuthenticationType type) { if (user == null) { return null; } if (user.disabled) { // user has been disabled logger.warn("Rejected {} authentication attempt by disabled account \"{}\"", type, user.username); return null; } return user; } protected void flagWicketSession(AuthenticationType authenticationType) { RequestCycle requestCycle = RequestCycle.get(); if (requestCycle != null) { // flag the Wicket session, if this is a Wicket request GitBlitWebSession session = GitBlitWebSession.get(); session.authenticationType = authenticationType; } } /** * Authenticate a user based on a username and password. * * @see IUserService.authenticate(String, char[]) * @param username * @param password * @return a user object or null */ @Override public UserModel authenticate(String username, char[] password) { if (StringUtils.isEmpty(username)) { // can not authenticate empty username return null; } String usernameDecoded = StringUtils.decodeUsername(username); String pw = new String(password); if (StringUtils.isEmpty(pw)) { // can not authenticate empty password return null; } UserModel user = userManager.getUserModel(usernameDecoded); // try local authentication if (user != null && user.isLocalAccount()) { return authenticateLocal(user, password); } // try registered external authentication providers for (AuthenticationProvider provider : authenticationProviders) { if (provider instanceof UsernamePasswordAuthenticationProvider) { UserModel returnedUser = provider.authenticate(usernameDecoded, password); if (returnedUser != null) { // user authenticated returnedUser.accountType = provider.getAccountType(); return validateAuthentication(returnedUser, AuthenticationType.CREDENTIALS); } } } // could not authenticate locally or with a provider return null; } /** * Returns a UserModel if local authentication succeeds. * * @param user * @param password * @return a UserModel if local authentication succeeds, null otherwise */ protected UserModel authenticateLocal(UserModel user, char [] password) { UserModel returnedUser = null; if (user.password.startsWith(StringUtils.MD5_TYPE)) { // password digest String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password)); if (user.password.equalsIgnoreCase(md5)) { returnedUser = user; } } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) { // username+password digest String md5 = StringUtils.COMBINED_MD5_TYPE + StringUtils.getMD5(user.username.toLowerCase() + new String(password)); if (user.password.equalsIgnoreCase(md5)) { returnedUser = user; } } else if (user.password.equals(new String(password))) { // plain-text password returnedUser = user; } return validateAuthentication(returnedUser, AuthenticationType.CREDENTIALS); } /** * Returns the Gitlbit cookie in the request. * * @param request * @return the Gitblit cookie for the request or null if not found */ @Override public String getCookie(HttpServletRequest request) { if (settings.getBoolean(Keys.web.allowCookieAuthentication, true)) { Cookie[] cookies = request.getCookies(); if (cookies != null && cookies.length > 0) { for (Cookie cookie : cookies) { if (cookie.getName().equals(Constants.NAME)) { String value = cookie.getValue(); return value; } } } } return null; } /** * Sets a cookie for the specified user. * * @param response * @param user */ @Override public void setCookie(HttpServletResponse response, UserModel user) { if (settings.getBoolean(Keys.web.allowCookieAuthentication, true)) { GitBlitWebSession session = GitBlitWebSession.get(); boolean standardLogin = session.authenticationType.isStandard(); if (standardLogin) { Cookie userCookie; if (user == null) { // clear cookie for logout userCookie = new Cookie(Constants.NAME, ""); } else { // set cookie for login String cookie = userManager.getCookie(user); if (StringUtils.isEmpty(cookie)) { // create empty cookie userCookie = new Cookie(Constants.NAME, ""); } else { // create real cookie userCookie = new Cookie(Constants.NAME, cookie); // expire the cookie in 7 days userCookie.setMaxAge((int) TimeUnit.DAYS.toSeconds(7)); } } userCookie.setPath("/"); response.addCookie(userCookie); } } } /** * Logout a user. * * @param user */ @Override public void logout(HttpServletResponse response, UserModel user) { setCookie(response, null); } /** * Returns true if the user's credentials can be changed. * * @param user * @return true if the user service supports credential changes */ @Override public boolean supportsCredentialChanges(UserModel user) { return (user != null && user.isLocalAccount()) || findProvider(user).supportsCredentialChanges(); } /** * Returns true if the user's display name can be changed. * * @param user * @return true if the user service supports display name changes */ @Override public boolean supportsDisplayNameChanges(UserModel user) { return (user != null && user.isLocalAccount()) || findProvider(user).supportsDisplayNameChanges(); } /** * Returns true if the user's email address can be changed. * * @param user * @return true if the user service supports email address changes */ @Override public boolean supportsEmailAddressChanges(UserModel user) { return (user != null && user.isLocalAccount()) || findProvider(user).supportsEmailAddressChanges(); } /** * Returns true if the user's team memberships can be changed. * * @param user * @return true if the user service supports team membership changes */ @Override public boolean supportsTeamMembershipChanges(UserModel user) { return (user != null && user.isLocalAccount()) || findProvider(user).supportsTeamMembershipChanges(); } /** * Returns true if the team memberships can be changed. * * @param user * @return true if the team membership can be changed */ @Override public boolean supportsTeamMembershipChanges(TeamModel team) { return (team != null && team.isLocalTeam()) || findProvider(team).supportsTeamMembershipChanges(); } protected AuthenticationProvider findProvider(UserModel user) { for (AuthenticationProvider provider : authenticationProviders) { if (provider.getAccountType().equals(user.accountType)) { return provider; } } return AuthenticationProvider.NULL_PROVIDER; } protected AuthenticationProvider findProvider(TeamModel team) { for (AuthenticationProvider provider : authenticationProviders) { if (provider.getAccountType().equals(team.accountType)) { return provider; } } return AuthenticationProvider.NULL_PROVIDER; } }