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