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