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 <nelson.silva@inevo.pt>
060 * @author <a href="mailto:tdelprat@nuxeo.com">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}