Line data Source code
1 : // Copyright (C) 2015 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.httpd.auth.oauth; 16 : 17 : import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; 18 : 19 : import com.google.common.base.CharMatcher; 20 : import com.google.common.base.Strings; 21 : import com.google.common.flogger.FluentLogger; 22 : import com.google.common.io.BaseEncoding; 23 : import com.google.gerrit.auth.oauth.OAuthTokenCache; 24 : import com.google.gerrit.entities.Account; 25 : import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 26 : import com.google.gerrit.extensions.auth.oauth.OAuthToken; 27 : import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 28 : import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; 29 : import com.google.gerrit.extensions.registration.DynamicItem; 30 : import com.google.gerrit.extensions.restapi.Url; 31 : import com.google.gerrit.httpd.CanonicalWebUrl; 32 : import com.google.gerrit.httpd.WebSession; 33 : import com.google.gerrit.server.IdentifiedUser; 34 : import com.google.gerrit.server.account.AccountException; 35 : import com.google.gerrit.server.account.AccountManager; 36 : import com.google.gerrit.server.account.AuthRequest; 37 : import com.google.gerrit.server.account.AuthResult; 38 : import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory; 39 : import com.google.inject.Inject; 40 : import com.google.inject.Provider; 41 : import com.google.inject.servlet.SessionScoped; 42 : import java.io.IOException; 43 : import java.security.NoSuchAlgorithmException; 44 : import java.security.SecureRandom; 45 : import java.util.Optional; 46 : import javax.servlet.ServletRequest; 47 : import javax.servlet.http.HttpServletRequest; 48 : import javax.servlet.http.HttpServletResponse; 49 : import org.eclipse.jgit.errors.ConfigInvalidException; 50 : 51 : @SessionScoped 52 : /* OAuth protocol implementation */ 53 : class OAuthSession { 54 0 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 55 : 56 0 : private static final SecureRandom randomState = newRandomGenerator(); 57 : private final String state; 58 : private final DynamicItem<WebSession> webSession; 59 : private final Provider<IdentifiedUser> identifiedUser; 60 : private final AccountManager accountManager; 61 : private final CanonicalWebUrl urlProvider; 62 : private final OAuthTokenCache tokenCache; 63 : private OAuthServiceProvider serviceProvider; 64 : private OAuthUserInfo user; 65 : private Account.Id accountId; 66 : private String redirectToken; 67 : private boolean linkMode; 68 : private final ExternalIdKeyFactory externalIdKeyFactory; 69 : private final AuthRequest.Factory authRequestFactory; 70 : 71 : @Inject 72 : OAuthSession( 73 : DynamicItem<WebSession> webSession, 74 : Provider<IdentifiedUser> identifiedUser, 75 : AccountManager accountManager, 76 : CanonicalWebUrl urlProvider, 77 : OAuthTokenCache tokenCache, 78 : ExternalIdKeyFactory externalIdKeyFactory, 79 0 : AuthRequest.Factory authRequestFactory) { 80 0 : this.state = generateRandomState(); 81 0 : this.identifiedUser = identifiedUser; 82 0 : this.webSession = webSession; 83 0 : this.accountManager = accountManager; 84 0 : this.urlProvider = urlProvider; 85 0 : this.tokenCache = tokenCache; 86 0 : this.externalIdKeyFactory = externalIdKeyFactory; 87 0 : this.authRequestFactory = authRequestFactory; 88 0 : } 89 : 90 : boolean isLoggedIn() { 91 0 : return user != null; 92 : } 93 : 94 : boolean isOAuthFinal(HttpServletRequest request) { 95 0 : return Strings.emptyToNull(request.getParameter("code")) != null; 96 : } 97 : 98 : boolean login( 99 : HttpServletRequest request, HttpServletResponse response, OAuthServiceProvider oauth) 100 : throws IOException { 101 0 : logger.atFine().log("Login %s", this); 102 : 103 0 : if (isOAuthFinal(request)) { 104 0 : if (!checkState(request)) { 105 0 : response.sendError(HttpServletResponse.SC_NOT_FOUND); 106 0 : return false; 107 : } 108 : 109 0 : logger.atFine().log("Login-Retrieve-User %s", this); 110 0 : OAuthToken token = oauth.getAccessToken(new OAuthVerifier(request.getParameter("code"))); 111 0 : user = oauth.getUserInfo(token); 112 : 113 0 : if (isLoggedIn()) { 114 0 : logger.atFine().log("Login-SUCCESS %s", this); 115 0 : authenticateAndRedirect(request, response, token); 116 0 : return true; 117 : } 118 0 : response.sendError(SC_UNAUTHORIZED); 119 0 : return false; 120 : } 121 0 : logger.atFine().log("Login-PHASE1 %s", this); 122 0 : redirectToken = request.getRequestURI(); 123 : // We are here in content of filter. 124 : // Due to this Jetty limitation: 125 : // https://bz.apache.org/bugzilla/show_bug.cgi?id=28323 126 : // we cannot use LoginUrlToken.getToken() method, 127 : // because it relies on getPathInfo() and it is always null here. 128 0 : redirectToken = redirectToken.substring(request.getContextPath().length()); 129 0 : response.sendRedirect(oauth.getAuthorizationUrl() + "&state=" + state); 130 0 : return false; 131 : } 132 : 133 : private void authenticateAndRedirect( 134 : HttpServletRequest req, HttpServletResponse rsp, OAuthToken token) throws IOException { 135 0 : AuthRequest areq = authRequestFactory.create(externalIdKeyFactory.parse(user.getExternalId())); 136 : AuthResult arsp; 137 : try { 138 0 : String claimedIdentifier = user.getClaimedIdentity(); 139 0 : if (!Strings.isNullOrEmpty(claimedIdentifier)) { 140 0 : if (!authenticateWithIdentityClaimedDuringHandshake(areq, rsp, claimedIdentifier)) { 141 0 : return; 142 : } 143 0 : } else if (linkMode) { 144 0 : if (!authenticateWithLinkedIdentity(areq, rsp)) { 145 0 : return; 146 : } 147 : } 148 0 : areq.setUserName(user.getUserName()); 149 0 : areq.setEmailAddress(user.getEmailAddress()); 150 0 : areq.setDisplayName(user.getDisplayName()); 151 0 : arsp = accountManager.authenticate(areq); 152 : 153 0 : accountId = arsp.getAccountId(); 154 0 : tokenCache.put(accountId, token); 155 0 : } catch (AccountException e) { 156 0 : logger.atSevere().withCause(e).log("Unable to authenticate user \"%s\"", user); 157 0 : rsp.sendError(HttpServletResponse.SC_FORBIDDEN); 158 0 : return; 159 0 : } 160 : 161 0 : webSession.get().login(arsp, true); 162 0 : String suffix = redirectToken.substring(OAuthWebFilter.GERRIT_LOGIN.length() + 1); 163 0 : suffix = CharMatcher.anyOf("/").trimLeadingFrom(Url.decode(suffix)); 164 0 : StringBuilder rdr = new StringBuilder(urlProvider.get(req)); 165 0 : rdr.append(suffix); 166 0 : rsp.sendRedirect(rdr.toString()); 167 0 : } 168 : 169 : private boolean authenticateWithIdentityClaimedDuringHandshake( 170 : AuthRequest req, HttpServletResponse rsp, String claimedIdentifier) 171 : throws AccountException, IOException { 172 0 : Optional<Account.Id> claimedId = accountManager.lookup(claimedIdentifier); 173 0 : Optional<Account.Id> actualId = accountManager.lookup(user.getExternalId()); 174 0 : if (claimedId.isPresent() && actualId.isPresent()) { 175 0 : if (claimedId.get().equals(actualId.get())) { 176 : // Both link to the same account, that's what we expected. 177 0 : logger.atFine().log("OAuth2: claimed identity equals current id"); 178 : } else { 179 : // This is (for now) a fatal error. There are two records 180 : // for what might be the same user. 181 : // 182 0 : logger.atSevere().log( 183 : "OAuth accounts disagree over user identity:\n" 184 : + " Claimed ID: %s is %s\n" 185 : + " Delgate ID: %s is %s", 186 0 : claimedId.get(), claimedIdentifier, actualId.get(), user.getExternalId()); 187 0 : rsp.sendError(HttpServletResponse.SC_FORBIDDEN); 188 0 : return false; 189 : } 190 0 : } else if (claimedId.isPresent() && !actualId.isPresent()) { 191 : // Claimed account already exists: link to it. 192 : // 193 0 : logger.atInfo().log("OAuth2: linking claimed identity to %s", claimedId.get()); 194 : try { 195 0 : accountManager.link(claimedId.get(), req); 196 0 : } catch (ConfigInvalidException e) { 197 0 : logger.atSevere().log( 198 : "Cannot link: %s to user identity:\n Claimed ID: %s is %s", 199 0 : user.getExternalId(), claimedId.get(), claimedIdentifier); 200 0 : rsp.sendError(HttpServletResponse.SC_FORBIDDEN); 201 0 : return false; 202 0 : } 203 : } 204 0 : return true; 205 : } 206 : 207 : private boolean authenticateWithLinkedIdentity(AuthRequest areq, HttpServletResponse rsp) 208 : throws AccountException, IOException { 209 : try { 210 0 : accountManager.link(identifiedUser.get().getAccountId(), areq); 211 0 : } catch (ConfigInvalidException e) { 212 0 : logger.atSevere().log( 213 : "Cannot link: %s to user identity: %s", 214 0 : user.getExternalId(), identifiedUser.get().getAccountId()); 215 0 : rsp.sendError(HttpServletResponse.SC_FORBIDDEN); 216 0 : return false; 217 : } finally { 218 0 : linkMode = false; 219 : } 220 0 : return true; 221 : } 222 : 223 : void logout() { 224 0 : if (accountId != null) { 225 0 : tokenCache.remove(accountId); 226 0 : accountId = null; 227 : } 228 0 : user = null; 229 0 : redirectToken = null; 230 0 : serviceProvider = null; 231 0 : } 232 : 233 : private boolean checkState(ServletRequest request) { 234 0 : String s = Strings.nullToEmpty(request.getParameter("state")); 235 0 : if (!s.equals(state)) { 236 0 : logger.atSevere().log("Illegal request state '%s' on OAuthProtocol %s", s, this); 237 0 : return false; 238 : } 239 0 : return true; 240 : } 241 : 242 : private static SecureRandom newRandomGenerator() { 243 : try { 244 0 : return SecureRandom.getInstance("SHA1PRNG"); 245 0 : } catch (NoSuchAlgorithmException e) { 246 0 : throw new IllegalStateException("No SecureRandom available for GitHub authentication", e); 247 : } 248 : } 249 : 250 : private static String generateRandomState() { 251 0 : byte[] state = new byte[32]; 252 0 : randomState.nextBytes(state); 253 0 : return BaseEncoding.base64Url().encode(state); 254 : } 255 : 256 : @Override 257 : public String toString() { 258 0 : return "OAuthSession [token=" + tokenCache.get(accountId) + ", user=" + user + "]"; 259 : } 260 : 261 : public void setServiceProvider(OAuthServiceProvider provider) { 262 0 : this.serviceProvider = provider; 263 0 : } 264 : 265 : public OAuthServiceProvider getServiceProvider() { 266 0 : return serviceProvider; 267 : } 268 : 269 : public void setLinkMode(boolean linkMode) { 270 0 : this.linkMode = linkMode; 271 0 : } 272 : 273 : public boolean isLinkMode() { 274 0 : return linkMode; 275 : } 276 : }