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.openid; 16 : 17 : import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; 18 : 19 : import com.google.common.base.Strings; 20 : import com.google.common.flogger.FluentLogger; 21 : import com.google.common.io.BaseEncoding; 22 : import com.google.gerrit.entities.Account; 23 : import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; 24 : import com.google.gerrit.extensions.auth.oauth.OAuthToken; 25 : import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; 26 : import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; 27 : import com.google.gerrit.extensions.registration.DynamicItem; 28 : import com.google.gerrit.extensions.restapi.Url; 29 : import com.google.gerrit.httpd.CanonicalWebUrl; 30 : import com.google.gerrit.httpd.LoginUrlToken; 31 : import com.google.gerrit.httpd.WebSession; 32 : import com.google.gerrit.server.IdentifiedUser; 33 : import com.google.gerrit.server.account.AccountException; 34 : import com.google.gerrit.server.account.AccountManager; 35 : import com.google.gerrit.server.account.AuthRequest; 36 : import com.google.gerrit.server.account.AuthResult; 37 : import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory; 38 : import com.google.inject.Inject; 39 : import com.google.inject.Provider; 40 : import com.google.inject.servlet.SessionScoped; 41 : import java.io.IOException; 42 : import java.security.NoSuchAlgorithmException; 43 : import java.security.SecureRandom; 44 : import java.util.Optional; 45 : import javax.servlet.ServletRequest; 46 : import javax.servlet.http.HttpServletRequest; 47 : import javax.servlet.http.HttpServletResponse; 48 : import org.eclipse.jgit.errors.ConfigInvalidException; 49 : 50 : /** OAuth protocol implementation */ 51 : @SessionScoped 52 : class OAuthSessionOverOpenID { 53 0 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 54 : 55 : static final String GERRIT_LOGIN = "/login"; 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 OAuthServiceProvider serviceProvider; 63 : private OAuthToken token; 64 : private OAuthUserInfo user; 65 : private String redirectToken; 66 : private boolean linkMode; 67 : private final ExternalIdKeyFactory externalIdKeyFactory; 68 : private final AuthRequest.Factory authRequestFactory; 69 : 70 : @Inject 71 : OAuthSessionOverOpenID( 72 : DynamicItem<WebSession> webSession, 73 : Provider<IdentifiedUser> identifiedUser, 74 : AccountManager accountManager, 75 : CanonicalWebUrl urlProvider, 76 : ExternalIdKeyFactory externalIdKeyFactory, 77 0 : AuthRequest.Factory authRequestFactory) { 78 0 : this.state = generateRandomState(); 79 0 : this.webSession = webSession; 80 0 : this.identifiedUser = identifiedUser; 81 0 : this.accountManager = accountManager; 82 0 : this.urlProvider = urlProvider; 83 0 : this.externalIdKeyFactory = externalIdKeyFactory; 84 0 : this.authRequestFactory = authRequestFactory; 85 0 : } 86 : 87 : boolean isLoggedIn() { 88 0 : return token != null && user != null; 89 : } 90 : 91 : boolean isOAuthFinal(HttpServletRequest request) { 92 0 : return Strings.emptyToNull(request.getParameter("code")) != null; 93 : } 94 : 95 : boolean login( 96 : HttpServletRequest request, HttpServletResponse response, OAuthServiceProvider oauth) 97 : throws IOException { 98 0 : logger.atFine().log("Login %s", this); 99 : 100 0 : if (isOAuthFinal(request)) { 101 0 : if (!checkState(request)) { 102 0 : response.sendError(HttpServletResponse.SC_NOT_FOUND); 103 0 : return false; 104 : } 105 : 106 0 : logger.atFine().log("Login-Retrieve-User %s", this); 107 0 : token = oauth.getAccessToken(new OAuthVerifier(request.getParameter("code"))); 108 0 : user = oauth.getUserInfo(token); 109 : 110 0 : if (isLoggedIn()) { 111 0 : logger.atFine().log("Login-SUCCESS %s", this); 112 0 : authenticateAndRedirect(request, response); 113 0 : return true; 114 : } 115 0 : response.sendError(SC_UNAUTHORIZED); 116 0 : return false; 117 : } 118 0 : logger.atFine().log("Login-PHASE1 %s", this); 119 0 : redirectToken = LoginUrlToken.getToken(request); 120 0 : response.sendRedirect(oauth.getAuthorizationUrl() + "&state=" + state); 121 0 : return false; 122 : } 123 : 124 : private void authenticateAndRedirect(HttpServletRequest req, HttpServletResponse rsp) 125 : throws IOException { 126 0 : com.google.gerrit.server.account.AuthRequest areq = 127 0 : authRequestFactory.create(externalIdKeyFactory.parse(user.getExternalId())); 128 : AuthResult arsp; 129 : try { 130 0 : String claimedIdentifier = user.getClaimedIdentity(); 131 0 : Optional<Account.Id> actualId = accountManager.lookup(user.getExternalId()); 132 0 : Optional<Account.Id> claimedId = Optional.empty(); 133 : 134 : // We try to retrieve claimed identity. 135 : // For some reason, for example staging instance 136 : // it may deviate from the really old OpenID identity. 137 : // What we want to avoid in any event is to create new 138 : // account instead of linking to the existing one. 139 : // That why we query it here, not to lose linking mode. 140 0 : if (!Strings.isNullOrEmpty(claimedIdentifier)) { 141 0 : claimedId = accountManager.lookup(claimedIdentifier); 142 0 : if (!claimedId.isPresent()) { 143 0 : logger.atFine().log("Claimed identity is unknown"); 144 : } 145 : } 146 : 147 : // Use case 1: claimed identity was provided during handshake phase 148 : // and user account exists for this identity 149 0 : if (claimedId.isPresent()) { 150 0 : logger.atFine().log("Claimed identity is set and is known"); 151 0 : if (actualId.isPresent()) { 152 0 : if (claimedId.get().equals(actualId.get())) { 153 : // Both link to the same account, that's what we expected. 154 0 : logger.atFine().log("Both link to the same account. All is fine."); 155 : } else { 156 : // This is (for now) a fatal error. There are two records 157 : // for what might be the same user. The admin would have to 158 : // link the accounts manually. 159 0 : logger.atFine().log( 160 : "OAuth accounts disagree over user identity:\n" 161 : + " Claimed ID: %s is %s\n" 162 : + " Delgate ID: %s is %s", 163 0 : claimedId.get(), claimedIdentifier, actualId.get(), user.getExternalId()); 164 0 : rsp.sendError(HttpServletResponse.SC_FORBIDDEN); 165 0 : return; 166 : } 167 : } else { 168 : // Claimed account already exists: link to it. 169 0 : logger.atFine().log("Claimed account already exists: link to it."); 170 : try { 171 0 : accountManager.link(claimedId.get(), areq); 172 0 : } catch (ConfigInvalidException e) { 173 0 : logger.atSevere().log( 174 : "Cannot link: %s to user identity:\n Claimed ID: %s is %s", 175 0 : user.getExternalId(), claimedId.get(), claimedIdentifier); 176 0 : rsp.sendError(HttpServletResponse.SC_FORBIDDEN); 177 0 : return; 178 0 : } 179 : } 180 0 : } else if (linkMode) { 181 : // Use case 2: link mode activated from the UI 182 0 : Account.Id accountId = identifiedUser.get().getAccountId(); 183 : try { 184 0 : logger.atFine().log("Linking \"%s\" to \"%s\"", user.getExternalId(), accountId); 185 0 : accountManager.link(accountId, areq); 186 0 : } catch (ConfigInvalidException e) { 187 0 : logger.atSevere().log( 188 0 : "Cannot link: %s to user identity: %s", user.getExternalId(), accountId); 189 0 : rsp.sendError(HttpServletResponse.SC_FORBIDDEN); 190 0 : return; 191 : } finally { 192 0 : linkMode = false; 193 : } 194 : } 195 0 : areq.setUserName(user.getUserName()); 196 0 : areq.setEmailAddress(user.getEmailAddress()); 197 0 : areq.setDisplayName(user.getDisplayName()); 198 0 : arsp = accountManager.authenticate(areq); 199 0 : } catch (AccountException e) { 200 0 : logger.atSevere().withCause(e).log("Unable to authenticate user \"%s\"", user); 201 0 : rsp.sendError(HttpServletResponse.SC_FORBIDDEN); 202 0 : return; 203 0 : } 204 : 205 0 : webSession.get().login(arsp, true); 206 0 : StringBuilder rdr = new StringBuilder(urlProvider.get(req)); 207 0 : rdr.append(Url.decode(redirectToken)); 208 0 : rsp.sendRedirect(rdr.toString()); 209 0 : } 210 : 211 : void logout() { 212 0 : token = null; 213 0 : user = null; 214 0 : redirectToken = null; 215 0 : serviceProvider = null; 216 0 : } 217 : 218 : private boolean checkState(ServletRequest request) { 219 0 : String s = Strings.nullToEmpty(request.getParameter("state")); 220 0 : if (!s.equals(state)) { 221 0 : logger.atSevere().log("Illegal request state '%s' on OAuthProtocol %s", s, this); 222 0 : return false; 223 : } 224 0 : return true; 225 : } 226 : 227 : private static SecureRandom newRandomGenerator() { 228 : try { 229 0 : return SecureRandom.getInstance("SHA1PRNG"); 230 0 : } catch (NoSuchAlgorithmException e) { 231 0 : throw new IllegalStateException("No SecureRandom available for GitHub authentication", e); 232 : } 233 : } 234 : 235 : private static String generateRandomState() { 236 0 : byte[] state = new byte[32]; 237 0 : randomState.nextBytes(state); 238 0 : return BaseEncoding.base64Url().encode(state); 239 : } 240 : 241 : @Override 242 : public String toString() { 243 0 : return "OAuthSession [token=" + token + ", user=" + user + "]"; 244 : } 245 : 246 : public void setServiceProvider(OAuthServiceProvider provider) { 247 0 : this.serviceProvider = provider; 248 0 : } 249 : 250 : public OAuthServiceProvider getServiceProvider() { 251 0 : return serviceProvider; 252 : } 253 : 254 : public void setLinkMode(boolean linkMode) { 255 0 : this.linkMode = linkMode; 256 0 : } 257 : 258 : public boolean isLinkMode() { 259 0 : return linkMode; 260 : } 261 : }