001/* 002 * (C) Copyright 2006-2013 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Nelson Silva 018 */ 019 020package org.nuxeo.ecm.platform.oauth2.openid; 021 022import java.io.IOException; 023import java.io.StringReader; 024import java.lang.reflect.Constructor; 025import java.math.BigInteger; 026import java.security.SecureRandom; 027 028import javax.servlet.http.HttpServletRequest; 029 030import org.apache.commons.io.IOUtils; 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033import org.nuxeo.ecm.platform.oauth2.openid.auth.EmailBasedUserResolver; 034import org.nuxeo.ecm.platform.oauth2.openid.auth.OpenIDConnectAuthenticator; 035import org.nuxeo.ecm.platform.oauth2.openid.auth.OpenIDUserInfo; 036import org.nuxeo.ecm.platform.oauth2.openid.auth.UserMapperResolver; 037import org.nuxeo.ecm.platform.oauth2.openid.auth.UserResolver; 038import org.nuxeo.ecm.platform.oauth2.providers.NuxeoOAuth2ServiceProvider; 039import org.nuxeo.ecm.platform.oauth2.providers.OAuth2ServiceProvider; 040import org.nuxeo.ecm.platform.ui.web.auth.service.LoginProviderLinkComputer; 041 042import com.google.api.client.auth.oauth2.AuthorizationCodeFlow; 043import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl; 044import com.google.api.client.auth.oauth2.TokenResponse; 045import com.google.api.client.http.GenericUrl; 046import com.google.api.client.http.HttpMediaType; 047import com.google.api.client.http.HttpRequest; 048import com.google.api.client.http.HttpRequestFactory; 049import com.google.api.client.http.HttpResponse; 050import com.google.api.client.http.HttpTransport; 051import com.google.api.client.http.javanet.NetHttpTransport; 052import com.google.api.client.json.JsonFactory; 053import com.google.api.client.json.JsonObjectParser; 054import com.google.api.client.json.jackson2.JacksonFactory; 055 056/** 057 * Class that holds info about an OpenID provider, this includes an OAuth Provider as well as urls and icons 058 * 059 * @author Nelson Silva <[email protected]> 060 * @author <a href="mailto:[email protected]">Tiry</a> 061 */ 062public class OpenIDConnectProvider implements LoginProviderLinkComputer { 063 064 protected static final Log log = LogFactory.getLog(OpenIDConnectProvider.class); 065 066 /** Global instance of the HTTP transport. */ 067 private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); 068 069 /** Global instance of the JSON factory. */ 070 private static final JsonFactory JSON_FACTORY = new JacksonFactory(); 071 072 private boolean enabled = true; 073 074 OAuth2ServiceProvider oauth2Provider; 075 076 private String userInfoURL; 077 078 private String icon; 079 080 protected RedirectUriResolver redirectUriResolver; 081 082 protected UserResolver userResolver; 083 084 protected String userMapper; 085 086 private String accessTokenKey; 087 088 private Class<? extends OpenIDUserInfo> openIdUserInfoClass; 089 090 public OpenIDConnectProvider(OAuth2ServiceProvider oauth2Provider, String accessTokenKey, String userInfoURL, 091 Class<? extends OpenIDUserInfo> openIdUserInfoClass, String icon, boolean enabled, 092 RedirectUriResolver redirectUriResolver, Class<? extends UserResolver> userResolverClass, String userMapper) { 093 this.oauth2Provider = oauth2Provider; 094 this.userInfoURL = userInfoURL; 095 this.openIdUserInfoClass = openIdUserInfoClass; 096 this.icon = icon; 097 this.enabled = enabled; 098 this.accessTokenKey = accessTokenKey; 099 this.redirectUriResolver = redirectUriResolver; 100 101 try { 102 if (userResolverClass == null) { 103 if (userMapper != null) { 104 userResolver = new UserMapperResolver(this, userMapper); 105 } else { 106 userResolver = new EmailBasedUserResolver(this); 107 } 108 } else { 109 Constructor<? extends UserResolver> c = userResolverClass.getConstructor(OpenIDConnectProvider.class); 110 userResolver = c.newInstance(this); 111 } 112 } catch (ReflectiveOperationException e) { 113 log.error("Failed to instantiate UserResolver", e); 114 } 115 } 116 117 public String getRedirectUri(HttpServletRequest req) { 118 return redirectUriResolver.getRedirectUri(this, req); 119 } 120 121 /** 122 * Create a state token to prevent request forgery. Store it in the session for later validation. 123 * 124 * @param HttpServletRequest request 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 */ 138 public boolean verifyStateToken(HttpServletRequest request) { 139 return request.getParameter(OpenIDConnectAuthenticator.STATE_URL_PARAM_NAME) 140 .equals(request.getSession().getAttribute( 141 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 return authorizationUrl.build(); 154 } 155 156 public String getName() { 157 return oauth2Provider != null ? oauth2Provider.getServiceName() : null; 158 } 159 160 public String getIcon() { 161 return icon; 162 } 163 164 public String getAccessToken(HttpServletRequest req, String code) { 165 String accessToken = null; 166 167 HttpResponse response = null; 168 169 try { 170 AuthorizationCodeFlow flow = ((NuxeoOAuth2ServiceProvider) oauth2Provider).getAuthorizationCodeFlow(); 171 172 String redirectUri = getRedirectUri(req); 173 response = flow.newTokenRequest(code).setRedirectUri(redirectUri).executeUnparsed(); 174 } catch (IOException e) { 175 log.error("Error during OAuth2 Authorization", e); 176 return null; 177 } 178 179 HttpMediaType mediaType = response.getMediaType(); 180 if (mediaType != null && "json".equals(mediaType.getSubType())) { 181 // Try to parse as json 182 try { 183 TokenResponse tokenResponse = response.parseAs(TokenResponse.class); 184 accessToken = tokenResponse.getAccessToken(); 185 } catch (IOException e) { 186 log.warn("Unable to parse accesstoken as JSON", e); 187 } 188 } else { 189 // Fallback as plain text format 190 try { 191 String[] params = response.parseAsString().split("&"); 192 for (String param : params) { 193 String[] kv = param.split("="); 194 if (kv[0].equals("access_token")) { 195 accessToken = kv[1]; // get the token 196 break; 197 } 198 } 199 } catch (IOException e) { 200 log.warn("Unable to parse accesstoken as plain text", e); 201 } 202 } 203 204 return accessToken; 205 } 206 207 public OpenIDUserInfo getUserInfo(String accessToken) { 208 OpenIDUserInfo userInfo = null; 209 210 HttpRequestFactory requestFactory = HTTP_TRANSPORT.createRequestFactory(request -> request.setParser(new JsonObjectParser( 211 JSON_FACTORY))); 212 213 GenericUrl url = new GenericUrl(userInfoURL); 214 url.set(accessTokenKey, accessToken); 215 216 try { 217 HttpRequest request = requestFactory.buildGetRequest(url); 218 HttpResponse response = request.execute(); 219 String body = IOUtils.toString(response.getContent(), "UTF-8"); 220 log.debug(body); 221 userInfo = parseUserInfo(body); 222 223 } catch (IOException e) { 224 log.error("Unable to parse server response", e); 225 } 226 227 return userInfo; 228 } 229 230 public OpenIDUserInfo parseUserInfo(String userInfoJSON) throws IOException { 231 return new JsonObjectParser(JSON_FACTORY).parseAndClose(new StringReader(userInfoJSON), openIdUserInfoClass); 232 } 233 234 public boolean isEnabled() { 235 return enabled; 236 } 237 238 public UserResolver getUserResolver() { 239 return userResolver; 240 } 241 242 @Override 243 public String computeUrl(HttpServletRequest req, String requestedUrl) { 244 return getAuthenticationUrl(req, requestedUrl); 245 } 246}