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}