001/* 002 * (C) Copyright 2006-2013 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Nelson Silva 016 */ 017 018package org.nuxeo.ecm.platform.oauth2.openid; 019 020import java.io.IOException; 021import java.io.StringReader; 022import java.lang.reflect.Constructor; 023import java.math.BigInteger; 024import java.security.SecureRandom; 025 026import javax.servlet.http.HttpServletRequest; 027 028import org.apache.commons.io.IOUtils; 029import org.apache.commons.lang.StringUtils; 030import org.apache.commons.logging.Log; 031import org.apache.commons.logging.LogFactory; 032import org.nuxeo.ecm.platform.oauth2.openid.auth.EmailBasedUserResolver; 033import org.nuxeo.ecm.platform.oauth2.openid.auth.OpenIDConnectAuthenticator; 034import org.nuxeo.ecm.platform.oauth2.openid.auth.OpenIDUserInfo; 035import org.nuxeo.ecm.platform.oauth2.openid.auth.UserMapperResolver; 036import org.nuxeo.ecm.platform.oauth2.openid.auth.UserResolver; 037import org.nuxeo.ecm.platform.oauth2.providers.NuxeoOAuth2ServiceProvider; 038import org.nuxeo.ecm.platform.oauth2.providers.OAuth2ServiceProvider; 039import org.nuxeo.ecm.platform.ui.web.auth.service.LoginProviderLinkComputer; 040 041import com.google.api.client.auth.oauth2.AuthorizationCodeFlow; 042import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl; 043import com.google.api.client.auth.oauth2.TokenResponse; 044import com.google.api.client.http.GenericUrl; 045import com.google.api.client.http.HttpRequest; 046import com.google.api.client.http.HttpRequestFactory; 047import com.google.api.client.http.HttpRequestInitializer; 048import com.google.api.client.http.HttpResponse; 049import com.google.api.client.http.HttpTransport; 050import com.google.api.client.http.javanet.NetHttpTransport; 051import com.google.api.client.json.JsonFactory; 052import com.google.api.client.json.JsonObjectParser; 053import com.google.api.client.json.jackson.JacksonFactory; 054 055/** 056 * Class that holds info about an OpenID provider, this includes an OAuth Provider as well as urls and icons 057 * 058 * @author Nelson Silva <nelson.silva@inevo.pt> 059 * @author <a href="mailto:tdelprat@nuxeo.com">Tiry</a> 060 */ 061public class OpenIDConnectProvider implements LoginProviderLinkComputer { 062 063 protected static final Log log = LogFactory.getLog(OpenIDConnectProvider.class); 064 065 /** Global instance of the HTTP transport. */ 066 private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); 067 068 /** Global instance of the JSON factory. */ 069 private static final JsonFactory JSON_FACTORY = new JacksonFactory(); 070 071 private boolean enabled = true; 072 073 OAuth2ServiceProvider oauth2Provider; 074 075 private String userInfoURL; 076 077 private String icon; 078 079 protected RedirectUriResolver redirectUriResolver; 080 081 protected UserResolver userResolver; 082 083 protected String userMapper; 084 085 private String accessTokenKey; 086 087 private Class<? extends OpenIDUserInfo> openIdUserInfoClass; 088 089 public OpenIDConnectProvider(OAuth2ServiceProvider oauth2Provider, String accessTokenKey, String userInfoURL, 090 Class<? extends OpenIDUserInfo> openIdUserInfoClass, String icon, boolean enabled, 091 RedirectUriResolver redirectUriResolver, Class<? extends UserResolver> userResolverClass, String userMapper) { 092 this.oauth2Provider = oauth2Provider; 093 this.userInfoURL = userInfoURL; 094 this.openIdUserInfoClass = openIdUserInfoClass; 095 this.icon = icon; 096 this.enabled = enabled; 097 this.accessTokenKey = accessTokenKey; 098 this.redirectUriResolver = redirectUriResolver; 099 100 try { 101 if (userResolverClass == null) { 102 if (userMapper != null) { 103 userResolver = new UserMapperResolver(this, userMapper); 104 } else { 105 userResolver = new EmailBasedUserResolver(this); 106 } 107 } else { 108 Constructor<? extends UserResolver> c = userResolverClass.getConstructor(new Class[] { OpenIDConnectProvider.class }); 109 userResolver = c.newInstance(new Object[] { this }); 110 } 111 } catch (ReflectiveOperationException e) { 112 log.error("Failed to instantiate UserResolver", e); 113 } 114 } 115 116 public String getRedirectUri(HttpServletRequest req) { 117 return redirectUriResolver.getRedirectUri(this, req); 118 } 119 120 /** 121 * Create a state token to prevent request forgery. Store it in the session for later validation. 122 * 123 * @param HttpServletRequest request 124 * @return 125 */ 126 public String createStateToken(HttpServletRequest request) { 127 String state = new BigInteger(130, new SecureRandom()).toString(32); 128 request.getSession().setAttribute(OpenIDConnectAuthenticator.STATE_SESSION_ATTRIBUTE + "_" + getName(), state); 129 return state; 130 } 131 132 /** 133 * Ensure that this is no request forgery going on, and that the user sending us this connect request is the user 134 * that was supposed to. 135 * 136 * @param HttpServletRequest request 137 * @return 138 */ 139 public boolean verifyStateToken(HttpServletRequest request) { 140 return request.getParameter(OpenIDConnectAuthenticator.STATE_URL_PARAM_NAME).equals( 141 request.getSession().getAttribute(OpenIDConnectAuthenticator.STATE_SESSION_ATTRIBUTE + "_" + getName())); 142 } 143 144 public String getAuthenticationUrl(HttpServletRequest req, String requestedUrl) { 145 // redirect to the authorization flow 146 AuthorizationCodeFlow flow = ((NuxeoOAuth2ServiceProvider) oauth2Provider).getAuthorizationCodeFlow(); 147 AuthorizationCodeRequestUrl authorizationUrl = flow.newAuthorizationUrl(); // .setResponseTypes("token"); 148 authorizationUrl.setRedirectUri(getRedirectUri(req)); 149 150 String state = createStateToken(req); 151 authorizationUrl.setState(state); 152 153 String authUrl = authorizationUrl.build(); 154 155 return authUrl; 156 } 157 158 public String getName() { 159 return oauth2Provider.getServiceName(); 160 } 161 162 public String getIcon() { 163 return icon; 164 } 165 166 public String getAccessToken(HttpServletRequest req, String code) { 167 String accessToken = null; 168 169 HttpResponse response = null; 170 171 try { 172 AuthorizationCodeFlow flow = ((NuxeoOAuth2ServiceProvider) oauth2Provider).getAuthorizationCodeFlow(); 173 174 String redirectUri = getRedirectUri(req); 175 response = flow.newTokenRequest(code).setRedirectUri(redirectUri).executeUnparsed(); 176 } catch (IOException e) { 177 log.error("Error during OAuth2 Authorization", e); 178 } 179 180 String type = response.getContentType(); 181 182 try { 183 // Try to parse as json 184 TokenResponse tokenResponse = response.parseAs(TokenResponse.class); 185 accessToken = tokenResponse.getAccessToken(); 186 } catch (IOException e) { 187 log.debug("Unable to parse accesstoken as JSON", e); 188 } 189 190 if (StringUtils.isBlank(accessToken)) { 191 // Fallback as plain text format 192 try { 193 String str = response.parseAsString(); 194 String[] params = str.split("&"); 195 for (String param : params) { 196 String[] kv = param.split("="); 197 if (kv[0].equals("access_token")) { 198 accessToken = kv[1]; // get the token 199 break; 200 } 201 } 202 } catch (IOException e) { 203 log.warn("Unable to parse accesstoken as plain text", e); 204 } 205 } 206 207 if (StringUtils.isBlank(accessToken)) { 208 log.error("Unable to parse access token from response."); 209 } 210 211 return accessToken; 212 } 213 214 public OpenIDUserInfo getUserInfo(String accessToken) { 215 OpenIDUserInfo userInfo = null; 216 217 HttpRequestFactory requestFactory = HTTP_TRANSPORT.createRequestFactory(new HttpRequestInitializer() { 218 @Override 219 public void initialize(HttpRequest request) throws IOException { 220 request.setParser(new JsonObjectParser(JSON_FACTORY)); 221 } 222 }); 223 224 GenericUrl url = new GenericUrl(userInfoURL); 225 url.set(accessTokenKey, accessToken); 226 227 try { 228 HttpRequest request = requestFactory.buildGetRequest(url); 229 HttpResponse response = request.execute(); 230 String body = IOUtils.toString(response.getContent(), "UTF-8"); 231 log.debug(body); 232 userInfo = parseUserInfo(body); 233 234 } catch (IOException e) { 235 log.error("Unable to parse server response", e); 236 } 237 238 return userInfo; 239 } 240 241 public OpenIDUserInfo parseUserInfo(String userInfoJSON) throws IOException { 242 return new JsonObjectParser(JSON_FACTORY).parseAndClose(new StringReader(userInfoJSON), openIdUserInfoClass); 243 } 244 245 public boolean isEnabled() { 246 return enabled; 247 } 248 249 public UserResolver getUserResolver() { 250 return userResolver; 251 } 252 253 @Override 254 public String computeUrl(HttpServletRequest req, String requestedUrl) { 255 return getAuthenticationUrl(req, requestedUrl); 256 } 257}