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