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