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.logging.Log; 030import org.apache.commons.logging.LogFactory; 031import org.nuxeo.ecm.platform.oauth2.openid.auth.EmailBasedUserResolver; 032import org.nuxeo.ecm.platform.oauth2.openid.auth.OpenIDConnectAuthenticator; 033import org.nuxeo.ecm.platform.oauth2.openid.auth.OpenIDUserInfo; 034import org.nuxeo.ecm.platform.oauth2.openid.auth.UserMapperResolver; 035import org.nuxeo.ecm.platform.oauth2.openid.auth.UserResolver; 036import org.nuxeo.ecm.platform.oauth2.providers.NuxeoOAuth2ServiceProvider; 037import org.nuxeo.ecm.platform.oauth2.providers.OAuth2ServiceProvider; 038import org.nuxeo.ecm.platform.ui.web.auth.service.LoginProviderLinkComputer; 039 040import com.google.api.client.auth.oauth2.AuthorizationCodeFlow; 041import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl; 042import com.google.api.client.auth.oauth2.TokenResponse; 043import com.google.api.client.http.GenericUrl; 044import com.google.api.client.http.HttpMediaType; 045import com.google.api.client.http.HttpRequest; 046import com.google.api.client.http.HttpRequestFactory; 047import com.google.api.client.http.HttpResponse; 048import com.google.api.client.http.HttpTransport; 049import com.google.api.client.http.javanet.NetHttpTransport; 050import com.google.api.client.json.JsonFactory; 051import com.google.api.client.json.JsonObjectParser; 052import com.google.api.client.json.jackson.JacksonFactory; 053 054/** 055 * Class that holds info about an OpenID provider, this includes an OAuth Provider as well as urls and icons 056 * 057 * @author Nelson Silva <nelson.silva@inevo.pt> 058 * @author <a href="mailto:tdelprat@nuxeo.com">Tiry</a> 059 */ 060public class OpenIDConnectProvider implements LoginProviderLinkComputer { 061 062 protected static final Log log = LogFactory.getLog(OpenIDConnectProvider.class); 063 064 /** Global instance of the HTTP transport. */ 065 private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); 066 067 /** Global instance of the JSON factory. */ 068 private static final JsonFactory JSON_FACTORY = new JacksonFactory(); 069 070 private boolean enabled = true; 071 072 OAuth2ServiceProvider oauth2Provider; 073 074 private String userInfoURL; 075 076 private String icon; 077 078 protected RedirectUriResolver redirectUriResolver; 079 080 protected UserResolver userResolver; 081 082 protected String userMapper; 083 084 private String accessTokenKey; 085 086 private Class<? extends OpenIDUserInfo> openIdUserInfoClass; 087 088 public OpenIDConnectProvider(OAuth2ServiceProvider oauth2Provider, String accessTokenKey, String userInfoURL, 089 Class<? extends OpenIDUserInfo> openIdUserInfoClass, String icon, boolean enabled, 090 RedirectUriResolver redirectUriResolver, Class<? extends UserResolver> userResolverClass, String userMapper) { 091 this.oauth2Provider = oauth2Provider; 092 this.userInfoURL = userInfoURL; 093 this.openIdUserInfoClass = openIdUserInfoClass; 094 this.icon = icon; 095 this.enabled = enabled; 096 this.accessTokenKey = accessTokenKey; 097 this.redirectUriResolver = redirectUriResolver; 098 099 try { 100 if (userResolverClass == null) { 101 if (userMapper != null) { 102 userResolver = new UserMapperResolver(this, userMapper); 103 } else { 104 userResolver = new EmailBasedUserResolver(this); 105 } 106 } else { 107 Constructor<? extends UserResolver> c = userResolverClass.getConstructor(OpenIDConnectProvider.class); 108 userResolver = c.newInstance(this); 109 } 110 } catch (ReflectiveOperationException e) { 111 log.error("Failed to instantiate UserResolver", e); 112 } 113 } 114 115 public String getRedirectUri(HttpServletRequest req) { 116 return redirectUriResolver.getRedirectUri(this, req); 117 } 118 119 /** 120 * Create a state token to prevent request forgery. Store it in the session for later validation. 121 * 122 * @param HttpServletRequest request 123 */ 124 public String createStateToken(HttpServletRequest request) { 125 String state = new BigInteger(130, new SecureRandom()).toString(32); 126 request.getSession().setAttribute(OpenIDConnectAuthenticator.STATE_SESSION_ATTRIBUTE + "_" + getName(), state); 127 return state; 128 } 129 130 /** 131 * Ensure that this is no request forgery going on, and that the user sending us this connect request is the user 132 * that was supposed to. 133 * 134 * @param HttpServletRequest request 135 */ 136 public boolean verifyStateToken(HttpServletRequest request) { 137 return request.getParameter(OpenIDConnectAuthenticator.STATE_URL_PARAM_NAME) 138 .equals(request.getSession().getAttribute( 139 OpenIDConnectAuthenticator.STATE_SESSION_ATTRIBUTE + "_" + getName())); 140 } 141 142 public String getAuthenticationUrl(HttpServletRequest req, String requestedUrl) { 143 // redirect to the authorization flow 144 AuthorizationCodeFlow flow = ((NuxeoOAuth2ServiceProvider) oauth2Provider).getAuthorizationCodeFlow(); 145 AuthorizationCodeRequestUrl authorizationUrl = flow.newAuthorizationUrl(); // .setResponseTypes("token"); 146 authorizationUrl.setRedirectUri(getRedirectUri(req)); 147 148 String state = createStateToken(req); 149 authorizationUrl.setState(state); 150 151 return authorizationUrl.build(); 152 } 153 154 public String getName() { 155 return oauth2Provider.getServiceName(); 156 } 157 158 public String getIcon() { 159 return icon; 160 } 161 162 public String getAccessToken(HttpServletRequest req, String code) { 163 String accessToken = null; 164 165 HttpResponse response = null; 166 167 try { 168 AuthorizationCodeFlow flow = ((NuxeoOAuth2ServiceProvider) oauth2Provider).getAuthorizationCodeFlow(); 169 170 String redirectUri = getRedirectUri(req); 171 response = flow.newTokenRequest(code).setRedirectUri(redirectUri).executeUnparsed(); 172 } catch (IOException e) { 173 log.error("Error during OAuth2 Authorization", e); 174 return null; 175 } 176 177 HttpMediaType mediaType = response.getMediaType(); 178 if (mediaType != null && "json".equals(mediaType.getSubType())) { 179 // Try to parse as json 180 try { 181 TokenResponse tokenResponse = response.parseAs(TokenResponse.class); 182 accessToken = tokenResponse.getAccessToken(); 183 } catch (IOException e) { 184 log.warn("Unable to parse accesstoken as JSON", e); 185 } 186 } else { 187 // Fallback as plain text format 188 try { 189 String[] params = response.parseAsString().split("&"); 190 for (String param : params) { 191 String[] kv = param.split("="); 192 if (kv[0].equals("access_token")) { 193 accessToken = kv[1]; // get the token 194 break; 195 } 196 } 197 } catch (IOException e) { 198 log.warn("Unable to parse accesstoken as plain text", e); 199 } 200 } 201 202 return accessToken; 203 } 204 205 public OpenIDUserInfo getUserInfo(String accessToken) { 206 OpenIDUserInfo userInfo = null; 207 208 HttpRequestFactory requestFactory = HTTP_TRANSPORT.createRequestFactory(request -> request.setParser(new JsonObjectParser( 209 JSON_FACTORY))); 210 211 GenericUrl url = new GenericUrl(userInfoURL); 212 url.set(accessTokenKey, accessToken); 213 214 try { 215 HttpRequest request = requestFactory.buildGetRequest(url); 216 HttpResponse response = request.execute(); 217 String body = IOUtils.toString(response.getContent(), "UTF-8"); 218 log.debug(body); 219 userInfo = parseUserInfo(body); 220 221 } catch (IOException e) { 222 log.error("Unable to parse server response", e); 223 } 224 225 return userInfo; 226 } 227 228 public OpenIDUserInfo parseUserInfo(String userInfoJSON) throws IOException { 229 return new JsonObjectParser(JSON_FACTORY).parseAndClose(new StringReader(userInfoJSON), openIdUserInfoClass); 230 } 231 232 public boolean isEnabled() { 233 return enabled; 234 } 235 236 public UserResolver getUserResolver() { 237 return userResolver; 238 } 239 240 @Override 241 public String computeUrl(HttpServletRequest req, String requestedUrl) { 242 return getAuthenticationUrl(req, requestedUrl); 243 } 244}