/*
* (C) Copyright 2006-2013 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nelson Silva
*/
package org.nuxeo.ecm.platform.oauth2.openid;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Constructor;
import java.math.BigInteger;
import java.security.SecureRandom;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.platform.oauth2.openid.auth.EmailBasedUserResolver;
import org.nuxeo.ecm.platform.oauth2.openid.auth.OpenIDConnectAuthenticator;
import org.nuxeo.ecm.platform.oauth2.openid.auth.OpenIDUserInfo;
import org.nuxeo.ecm.platform.oauth2.openid.auth.UserMapperResolver;
import org.nuxeo.ecm.platform.oauth2.openid.auth.UserResolver;
import org.nuxeo.ecm.platform.oauth2.providers.NuxeoOAuth2ServiceProvider;
import org.nuxeo.ecm.platform.oauth2.providers.OAuth2ServiceProvider;
import org.nuxeo.ecm.platform.ui.web.auth.service.LoginProviderLinkComputer;
import com.google.api.client.auth.oauth2.AuthorizationCodeFlow;
import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpMediaType;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.json.jackson.JacksonFactory;
/**
* Class that holds info about an OpenID provider, this includes an OAuth Provider as well as urls and icons
*
* @author Nelson Silva <nelson.silva@inevo.pt>
* @author <a href="mailto:tdelprat@nuxeo.com">Tiry</a>
*/
public class OpenIDConnectProvider implements LoginProviderLinkComputer {
protected static final Log log = LogFactory.getLog(OpenIDConnectProvider.class);
/** Global instance of the HTTP transport. */
private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
/** Global instance of the JSON factory. */
private static final JsonFactory JSON_FACTORY = new JacksonFactory();
private boolean enabled = true;
OAuth2ServiceProvider oauth2Provider;
private String userInfoURL;
private String icon;
protected RedirectUriResolver redirectUriResolver;
protected UserResolver userResolver;
protected String userMapper;
private String accessTokenKey;
private Class<? extends OpenIDUserInfo> openIdUserInfoClass;
public OpenIDConnectProvider(OAuth2ServiceProvider oauth2Provider, String accessTokenKey, String userInfoURL,
Class<? extends OpenIDUserInfo> openIdUserInfoClass, String icon, boolean enabled,
RedirectUriResolver redirectUriResolver, Class<? extends UserResolver> userResolverClass, String userMapper) {
this.oauth2Provider = oauth2Provider;
this.userInfoURL = userInfoURL;
this.openIdUserInfoClass = openIdUserInfoClass;
this.icon = icon;
this.enabled = enabled;
this.accessTokenKey = accessTokenKey;
this.redirectUriResolver = redirectUriResolver;
try {
if (userResolverClass == null) {
if (userMapper != null) {
userResolver = new UserMapperResolver(this, userMapper);
} else {
userResolver = new EmailBasedUserResolver(this);
}
} else {
Constructor<? extends UserResolver> c = userResolverClass.getConstructor(OpenIDConnectProvider.class);
userResolver = c.newInstance(this);
}
} catch (ReflectiveOperationException e) {
log.error("Failed to instantiate UserResolver", e);
}
}
public String getRedirectUri(HttpServletRequest req) {
return redirectUriResolver.getRedirectUri(this, req);
}
/**
* Create a state token to prevent request forgery. Store it in the session for later validation.
*
* @param HttpServletRequest request
*/
public String createStateToken(HttpServletRequest request) {
String state = new BigInteger(130, new SecureRandom()).toString(32);
request.getSession().setAttribute(OpenIDConnectAuthenticator.STATE_SESSION_ATTRIBUTE + "_" + getName(), state);
return state;
}
/**
* Ensure that this is no request forgery going on, and that the user sending us this connect request is the user
* that was supposed to.
*
* @param HttpServletRequest request
*/
public boolean verifyStateToken(HttpServletRequest request) {
return request.getParameter(OpenIDConnectAuthenticator.STATE_URL_PARAM_NAME)
.equals(request.getSession().getAttribute(
OpenIDConnectAuthenticator.STATE_SESSION_ATTRIBUTE + "_" + getName()));
}
public String getAuthenticationUrl(HttpServletRequest req, String requestedUrl) {
// redirect to the authorization flow
AuthorizationCodeFlow flow = ((NuxeoOAuth2ServiceProvider) oauth2Provider).getAuthorizationCodeFlow();
AuthorizationCodeRequestUrl authorizationUrl = flow.newAuthorizationUrl(); // .setResponseTypes("token");
authorizationUrl.setRedirectUri(getRedirectUri(req));
String state = createStateToken(req);
authorizationUrl.setState(state);
return authorizationUrl.build();
}
public String getName() {
return oauth2Provider.getServiceName();
}
public String getIcon() {
return icon;
}
public String getAccessToken(HttpServletRequest req, String code) {
String accessToken = null;
HttpResponse response = null;
try {
AuthorizationCodeFlow flow = ((NuxeoOAuth2ServiceProvider) oauth2Provider).getAuthorizationCodeFlow();
String redirectUri = getRedirectUri(req);
response = flow.newTokenRequest(code).setRedirectUri(redirectUri).executeUnparsed();
} catch (IOException e) {
log.error("Error during OAuth2 Authorization", e);
return null;
}
HttpMediaType mediaType = response.getMediaType();
if (mediaType != null && "json".equals(mediaType.getSubType())) {
// Try to parse as json
try {
TokenResponse tokenResponse = response.parseAs(TokenResponse.class);
accessToken = tokenResponse.getAccessToken();
} catch (IOException e) {
log.warn("Unable to parse accesstoken as JSON", e);
}
} else {
// Fallback as plain text format
try {
String[] params = response.parseAsString().split("&");
for (String param : params) {
String[] kv = param.split("=");
if (kv[0].equals("access_token")) {
accessToken = kv[1]; // get the token
break;
}
}
} catch (IOException e) {
log.warn("Unable to parse accesstoken as plain text", e);
}
}
return accessToken;
}
public OpenIDUserInfo getUserInfo(String accessToken) {
OpenIDUserInfo userInfo = null;
HttpRequestFactory requestFactory = HTTP_TRANSPORT.createRequestFactory(request -> request.setParser(new JsonObjectParser(
JSON_FACTORY)));
GenericUrl url = new GenericUrl(userInfoURL);
url.set(accessTokenKey, accessToken);
try {
HttpRequest request = requestFactory.buildGetRequest(url);
HttpResponse response = request.execute();
String body = IOUtils.toString(response.getContent(), "UTF-8");
log.debug(body);
userInfo = parseUserInfo(body);
} catch (IOException e) {
log.error("Unable to parse server response", e);
}
return userInfo;
}
public OpenIDUserInfo parseUserInfo(String userInfoJSON) throws IOException {
return new JsonObjectParser(JSON_FACTORY).parseAndClose(new StringReader(userInfoJSON), openIdUserInfoClass);
}
public boolean isEnabled() {
return enabled;
}
public UserResolver getUserResolver() {
return userResolver;
}
@Override
public String computeUrl(HttpServletRequest req, String requestedUrl) {
return getAuthenticationUrl(req, requestedUrl);
}
}