com.google.gerrit.httpd.ProjectOAuthFilter.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.httpd.ProjectOAuthFilter.java

Source

// Copyright (C) 2015 The Android Open Source Project
//
// 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.google.gerrit.httpd;

import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;

import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.DynamicMap.Entry;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Locale;
import java.util.NoSuchElementException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Authenticates the current user with an OAuth2 server.
 *
 * @see <a href="https://tools.ietf.org/rfc/rfc6750.txt">RFC 6750</a>
 */
@Singleton
class ProjectOAuthFilter implements Filter {

    private static final Logger log = LoggerFactory.getLogger(ProjectOAuthFilter.class);

    private static final String REALM_NAME = "Gerrit Code Review";
    private static final String AUTHORIZATION = "Authorization";
    private static final String BASIC = "Basic ";
    private static final String GIT_COOKIE_PREFIX = "git-";

    private final DynamicItem<WebSession> session;
    private final DynamicMap<OAuthLoginProvider> loginProviders;
    private final AccountCache accountCache;
    private final AccountManager accountManager;
    private final String gitOAuthProvider;
    private final boolean userNameToLowerCase;

    private String defaultAuthPlugin;
    private String defaultAuthProvider;

    @Inject
    ProjectOAuthFilter(DynamicItem<WebSession> session, DynamicMap<OAuthLoginProvider> pluginsProvider,
            AccountCache accountCache, AccountManager accountManager, @GerritServerConfig Config gerritConfig) {
        this.session = session;
        this.loginProviders = pluginsProvider;
        this.accountCache = accountCache;
        this.accountManager = accountManager;
        this.gitOAuthProvider = gerritConfig.getString("auth", null, "gitOAuthProvider");
        this.userNameToLowerCase = gerritConfig.getBoolean("auth", null, "userNameToLowerCase", false);
    }

    @Override
    public void init(FilterConfig config) throws ServletException {
        if (Strings.isNullOrEmpty(gitOAuthProvider)) {
            pickOnlyProvider();
        } else {
            pickConfiguredProvider();
        }
    }

    @Override
    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        Response rsp = new Response((HttpServletResponse) response);
        if (verify(req, rsp)) {
            chain.doFilter(req, rsp);
        }
    }

    private boolean verify(HttpServletRequest req, Response rsp) throws IOException {
        AuthInfo authInfo = null;

        // first check if there is a BASIC authentication header
        String hdr = req.getHeader(AUTHORIZATION);
        if (hdr != null && hdr.startsWith(BASIC)) {
            authInfo = extractAuthInfo(hdr, encoding(req));
            if (authInfo == null) {
                rsp.sendError(SC_UNAUTHORIZED);
                return false;
            }
        } else {
            // if there is no BASIC authentication header, check if there is
            // a cookie starting with the prefix "git-"
            Cookie cookie = findGitCookie(req);
            if (cookie != null) {
                authInfo = extractAuthInfo(cookie);
                if (authInfo == null) {
                    rsp.sendError(SC_UNAUTHORIZED);
                    return false;
                }
            } else {
                // if there is no authentication information at all, it might be
                // an anonymous connection, or there might be a session cookie
                return true;
            }
        }

        // if there is authentication information but no secret => 401
        if (Strings.isNullOrEmpty(authInfo.tokenOrSecret)) {
            rsp.sendError(SC_UNAUTHORIZED);
            return false;
        }

        AccountState who = accountCache.getByUsername(authInfo.username);
        if (who == null || !who.getAccount().isActive()) {
            log.warn("Authentication failed for " + authInfo.username
                    + ": account inactive or not provisioned in Gerrit");
            rsp.sendError(SC_UNAUTHORIZED);
            return false;
        }

        AuthRequest authRequest = AuthRequest.forExternalUser(authInfo.username);
        authRequest.setEmailAddress(who.getAccount().getPreferredEmail());
        authRequest.setDisplayName(who.getAccount().getFullName());
        authRequest.setPassword(authInfo.tokenOrSecret);
        authRequest.setAuthPlugin(authInfo.pluginName);
        authRequest.setAuthProvider(authInfo.exportName);

        try {
            AuthResult authResult = accountManager.authenticate(authRequest);
            WebSession ws = session.get();
            ws.setUserAccountId(authResult.getAccountId());
            ws.setAccessPathOk(AccessPath.GIT, true);
            ws.setAccessPathOk(AccessPath.REST_API, true);
            return true;
        } catch (AccountException e) {
            log.warn("Authentication failed for " + authInfo.username, e);
            rsp.sendError(SC_UNAUTHORIZED);
            return false;
        }
    }

    /**
     * Picks the only installed OAuth provider. If there is a multiude of providers available, the
     * actual provider must be determined from the authentication request.
     *
     * @throws ServletException if there is no {@code OAuthLoginProvider} installed at all.
     */
    private void pickOnlyProvider() throws ServletException {
        try {
            Entry<OAuthLoginProvider> loginProvider = Iterables.getOnlyElement(loginProviders);
            defaultAuthPlugin = loginProvider.getPluginName();
            defaultAuthProvider = loginProvider.getExportName();
        } catch (NoSuchElementException e) {
            throw new ServletException("No OAuth login provider installed");
        } catch (IllegalArgumentException e) {
            // multiple providers found => do not pick any
        }
    }

    /**
     * Picks the {@code OAuthLoginProvider} configured with <tt>auth.gitOAuthProvider</tt>.
     *
     * @throws ServletException if the configured provider was not found.
     */
    private void pickConfiguredProvider() throws ServletException {
        int splitPos = gitOAuthProvider.lastIndexOf(':');
        if (splitPos < 1 || splitPos == gitOAuthProvider.length() - 1) {
            // no colon at all or leading/trailing colon: malformed providerId
            throw new ServletException("OAuth login provider configuration is"
                    + " invalid: Must be of the form pluginName:providerName");
        }
        defaultAuthPlugin = gitOAuthProvider.substring(0, splitPos);
        defaultAuthProvider = gitOAuthProvider.substring(splitPos + 1);
        OAuthLoginProvider provider = loginProviders.get(defaultAuthPlugin, defaultAuthProvider);
        if (provider == null) {
            throw new ServletException("Configured OAuth login provider " + gitOAuthProvider + " wasn't installed");
        }
    }

    private AuthInfo extractAuthInfo(String hdr, String encoding) throws UnsupportedEncodingException {
        byte[] decoded = Base64.decodeBase64(hdr.substring(BASIC.length()));
        String usernamePassword = new String(decoded, encoding);
        int splitPos = usernamePassword.indexOf(':');
        if (splitPos < 1 || splitPos == usernamePassword.length() - 1) {
            return null;
        }
        return new AuthInfo(usernamePassword.substring(0, splitPos), usernamePassword.substring(splitPos + 1),
                defaultAuthPlugin, defaultAuthProvider);
    }

    private AuthInfo extractAuthInfo(Cookie cookie) throws UnsupportedEncodingException {
        String username = URLDecoder.decode(cookie.getName().substring(GIT_COOKIE_PREFIX.length()), UTF_8.name());
        String value = cookie.getValue();
        int splitPos = value.lastIndexOf('@');
        if (splitPos < 1 || splitPos == value.length() - 1) {
            // no providerId in the cookie value => assume default provider
            // note: a leading/trailing at sign is considered to belong to
            // the access token rather than being a separator
            return new AuthInfo(username, cookie.getValue(), defaultAuthPlugin, defaultAuthProvider);
        }
        String token = value.substring(0, splitPos);
        String providerId = value.substring(splitPos + 1);
        splitPos = providerId.lastIndexOf(':');
        if (splitPos < 1 || splitPos == providerId.length() - 1) {
            // no colon at all or leading/trailing colon: malformed providerId
            return null;
        }
        String pluginName = providerId.substring(0, splitPos);
        String exportName = providerId.substring(splitPos + 1);
        OAuthLoginProvider provider = loginProviders.get(pluginName, exportName);
        if (provider == null) {
            return null;
        }
        return new AuthInfo(username, token, pluginName, exportName);
    }

    private static String encoding(HttpServletRequest req) {
        return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
    }

    private static Cookie findGitCookie(HttpServletRequest req) {
        Cookie[] cookies = req.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().startsWith(GIT_COOKIE_PREFIX)) {
                    return cookie;
                }
            }
        }
        return null;
    }

    private class AuthInfo {
        private final String username;
        private final String tokenOrSecret;
        private final String pluginName;
        private final String exportName;

        private AuthInfo(String username, String tokenOrSecret, String pluginName, String exportName) {
            this.username = userNameToLowerCase ? username.toLowerCase(Locale.US) : username;
            this.tokenOrSecret = tokenOrSecret;
            this.pluginName = pluginName;
            this.exportName = exportName;
        }
    }

    private static class Response extends HttpServletResponseWrapper {
        private static final String WWW_AUTHENTICATE = "WWW-Authenticate";

        Response(HttpServletResponse rsp) {
            super(rsp);
        }

        private void status(int sc) {
            if (sc == SC_UNAUTHORIZED) {
                StringBuilder v = new StringBuilder();
                v.append(BASIC);
                v.append("realm=\"").append(REALM_NAME).append("\"");
                setHeader(WWW_AUTHENTICATE, v.toString());
            } else if (containsHeader(WWW_AUTHENTICATE)) {
                setHeader(WWW_AUTHENTICATE, null);
            }
        }

        @Override
        public void sendError(int sc, String msg) throws IOException {
            status(sc);
            super.sendError(sc, msg);
        }

        @Override
        public void sendError(int sc) throws IOException {
            status(sc);
            super.sendError(sc);
        }

        @Override
        @Deprecated
        public void setStatus(int sc, String sm) {
            status(sc);
            super.setStatus(sc, sm);
        }

        @Override
        public void setStatus(int sc) {
            status(sc);
            super.setStatus(sc);
        }
    }
}