001/*
002 * (C) Copyright 2006-2018 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 */
019package org.nuxeo.ecm.platform.oauth2.providers;
020
021import java.io.IOException;
022import java.io.Serializable;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028
029import javax.servlet.http.HttpServletRequest;
030
031import org.apache.commons.lang3.StringUtils;
032import org.nuxeo.ecm.core.api.NuxeoException;
033import org.nuxeo.ecm.platform.oauth2.tokens.NuxeoOAuth2Token;
034import org.nuxeo.ecm.platform.oauth2.tokens.OAuth2TokenStore;
035import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
036
037import com.google.api.client.auth.oauth2.AuthorizationCodeFlow;
038import com.google.api.client.auth.oauth2.BearerToken;
039import com.google.api.client.auth.oauth2.ClientParametersAuthentication;
040import com.google.api.client.auth.oauth2.Credential;
041import com.google.api.client.auth.oauth2.TokenResponse;
042import com.google.api.client.http.GenericUrl;
043import com.google.api.client.http.HttpExecuteInterceptor;
044import com.google.api.client.http.HttpTransport;
045import com.google.api.client.http.javanet.NetHttpTransport;
046import com.google.api.client.json.JsonFactory;
047import com.google.api.client.json.jackson2.JacksonFactory;
048
049public class NuxeoOAuth2ServiceProvider implements OAuth2ServiceProvider {
050
051    public static final String SCHEMA = "oauth2ServiceProvider";
052
053    /** Global instance of the HTTP transport. */
054    protected static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
055
056    /** Global instance of the JSON factory. */
057    protected static final JsonFactory JSON_FACTORY = new JacksonFactory();
058
059    public static final String CODE_URL_PARAMETER = "code";
060
061    public static final String ERROR_URL_PARAMETER = "error";
062
063    protected String serviceName;
064
065    protected Long id;
066
067    protected String description;
068
069    private String tokenServerURL;
070
071    private String authorizationServerURL;
072
073    protected String userAuthorizationURL;
074
075    private String clientId;
076
077    private String clientSecret;
078
079    private List<String> scopes;
080
081    private boolean enabled;
082
083    protected OAuth2ServiceUserStore serviceUserStore;
084
085    protected OAuth2TokenStore tokenStore;
086
087    @Override
088    public String getAuthorizationUrl(HttpServletRequest request) {
089        return getAuthorizationCodeFlow().newAuthorizationUrl().setRedirectUri(getCallbackUrl(request)).build();
090    }
091
092    @Override
093    public String getAuthorizationUrl(String serverURL) {
094        return getAuthorizationCodeFlow().newAuthorizationUrl().setRedirectUri(getCallbackUrl(serverURL)).build();
095    }
096
097    protected String getCallbackUrl(HttpServletRequest request) {
098        return getCallbackUrl(VirtualHostHelper.getBaseURL(request));
099    }
100
101    protected String getCallbackUrl(String serverURL) {
102        if (serverURL.endsWith("/")) {
103            serverURL = serverURL.substring(0, serverURL.length() - 1);
104        }
105
106        return serverURL + "/site/oauth2/" + serviceName + "/callback";
107    }
108
109    @Override
110    public Credential handleAuthorizationCallback(HttpServletRequest request) {
111
112        // Checking if there was an error such as the user denied access
113        String error = getError(request);
114        if (error != null) {
115            throw new NuxeoException("There was an error: \"" + error + "\".");
116        }
117
118        // Checking conditions on the "code" URL parameter
119        String code = getAuthorizationCode(request);
120        if (code == null) {
121            throw new NuxeoException("There is not code provided as QueryParam.");
122        }
123
124        try {
125            AuthorizationCodeFlow flow = getAuthorizationCodeFlow();
126
127            String redirectUri = getCallbackUrl(request);
128
129            TokenResponse tokenResponse = flow.newTokenRequest(code)
130                                              .setScopes(scopes.isEmpty() ? null : scopes) // some providers do not
131                                                                                           // support the 'scopes' param
132                                              .setRedirectUri(redirectUri)
133                                              .execute();
134
135            // Create a unique userId to use with the credential store
136            String userId = getOrCreateServiceUser(request, tokenResponse.getAccessToken());
137
138            return flow.createAndStoreCredential(tokenResponse, userId);
139        } catch (IOException e) {
140            throw new NuxeoException("Failed to retrieve credential", e);
141        }
142    }
143
144    /**
145     * Load a credential from the token store with the userId returned by getServiceUser() as key.
146     */
147    @Override
148    public Credential loadCredential(String user) {
149        String userId = getServiceUserId(user);
150        try {
151            return userId != null ? getAuthorizationCodeFlow().loadCredential(userId) : null;
152        } catch (IOException e) {
153            throw new NuxeoException("Failed to load credential for " + user, e);
154        }
155    }
156
157    /**
158     * Returns the userId to use for token entries. Should be overriden by subclasses wanting to rely on a different
159     * field as key.
160     */
161    protected String getServiceUserId(String key) {
162        Map<String, Serializable> filter = new HashMap<>();
163        filter.put(NuxeoOAuth2Token.KEY_NUXEO_LOGIN, key);
164        return getServiceUserStore().find(filter);
165    }
166
167    /**
168     * Retrieves or creates a service user. Should be overriden by subclasses wanting to rely on a different field as
169     * key.
170     */
171    protected String getOrCreateServiceUser(HttpServletRequest request, String accessToken) throws IOException {
172        String nuxeoLogin = request.getUserPrincipal().getName();
173        String userId = getServiceUserId(nuxeoLogin);
174        if (userId == null) {
175            userId = getServiceUserStore().store(nuxeoLogin);
176        }
177        return userId;
178    }
179
180    public AuthorizationCodeFlow getAuthorizationCodeFlow() {
181        Credential.AccessMethod method = BearerToken.authorizationHeaderAccessMethod();
182        GenericUrl tokenServerUrl = new GenericUrl(tokenServerURL);
183        HttpExecuteInterceptor clientAuthentication = new ClientParametersAuthentication(clientId, clientSecret);
184        String authorizationServerUrl = authorizationServerURL;
185
186        return new AuthorizationCodeFlow.Builder(method, HTTP_TRANSPORT, JSON_FACTORY, tokenServerUrl,
187                clientAuthentication, clientId, authorizationServerUrl).setScopes(scopes)
188                                                                       .setCredentialDataStore(getCredentialDataStore())
189                                                                       .build();
190    }
191
192    protected OAuth2ServiceUserStore getServiceUserStore() {
193        if (serviceUserStore == null) {
194            serviceUserStore = new OAuth2ServiceUserStore(serviceName);
195        }
196        return serviceUserStore;
197    }
198
199    public OAuth2TokenStore getCredentialDataStore() {
200        if (tokenStore == null) {
201            tokenStore = new OAuth2TokenStore(serviceName);
202        }
203        return tokenStore;
204    }
205
206    protected String getError(HttpServletRequest request) {
207        String error = request.getParameter(ERROR_URL_PARAMETER);
208        return StringUtils.isBlank(error) ? null : error;
209    }
210
211    // Checking conditions on the "code" URL parameter
212    protected String getAuthorizationCode(HttpServletRequest request) {
213        String code = request.getParameter(CODE_URL_PARAMETER);
214        return StringUtils.isBlank(code) ? null : code;
215    }
216
217    @Override
218    public String getServiceName() {
219        return serviceName;
220    }
221
222    @Override
223    public Long getId() {
224        return id;
225    }
226
227    @Override
228    public String getDescription() {
229        return description;
230    }
231
232    @Override
233    public String getTokenServerURL() {
234        return tokenServerURL;
235    }
236
237    @Override
238    public String getUserAuthorizationURL() {
239        return userAuthorizationURL;
240    }
241
242    @Override
243    public String getClientId() {
244        return clientId;
245    }
246
247    @Override
248    public String getClientSecret() {
249        return clientSecret;
250    }
251
252    @Override
253    public List<String> getScopes() {
254        return scopes;
255    }
256
257    @Override
258    public String getAuthorizationServerURL() {
259        return authorizationServerURL;
260    }
261
262    @Override
263    public boolean isEnabled() {
264        return enabled;
265    }
266
267    @Override
268    public void setEnabled(Boolean enabled) {
269        this.enabled = enabled;
270    }
271
272    @Override
273    public boolean isProviderAvailable() {
274        return isEnabled() && getClientSecret() != null && getClientId() != null;
275    }
276
277    @Override
278    public void setServiceName(String serviceName) {
279        this.serviceName = serviceName;
280    }
281
282    @Override
283    public void setId(Long id) {
284        this.id = id;
285    }
286
287    @Override
288    public void setDescription(String description) {
289        this.description = description;
290    }
291
292    @Override
293    public void setTokenServerURL(String tokenServerURL) {
294        this.tokenServerURL = tokenServerURL;
295    }
296
297    @Override
298    public void setUserAuthorizationURL(String userAuthorizationURL) {
299        this.userAuthorizationURL = userAuthorizationURL;
300    }
301
302    @Override
303    public void setAuthorizationServerURL(String authorizationServerURL) {
304        this.authorizationServerURL = authorizationServerURL;
305    }
306
307    @Override
308    public void setClientId(String clientId) {
309        this.clientId = clientId;
310    }
311
312    @Override
313    public void setClientSecret(String clientSecret) {
314        this.clientSecret = clientSecret;
315    }
316
317    @Override
318    public void setScopes(String... scopes) {
319        this.scopes = (scopes == null) ? Collections.emptyList() : Arrays.asList(scopes);
320    }
321}