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