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}