001/*
002 * (C) Copyright 2014 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 *     Arnaud Kervern
018 */
019package org.nuxeo.ecm.platform.oauth2.request;
020
021import static org.nuxeo.ecm.platform.oauth2.Constants.CLIENT_ID_PARAM;
022import static org.nuxeo.ecm.platform.oauth2.Constants.CODE_RESPONSE_TYPE;
023import static org.nuxeo.ecm.platform.oauth2.Constants.REDIRECT_URI_PARAM;
024import static org.nuxeo.ecm.platform.oauth2.Constants.RESPONSE_TYPE_PARAM;
025import static org.nuxeo.ecm.platform.oauth2.Constants.SCOPE_PARAM;
026
027import java.io.Serializable;
028import java.security.Principal;
029import java.util.Date;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Map;
033
034import javax.servlet.http.HttpServletRequest;
035
036import org.apache.commons.collections.CollectionUtils;
037import org.apache.commons.lang.RandomStringUtils;
038import org.apache.commons.lang.StringUtils;
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
041import org.nuxeo.ecm.core.transientstore.api.TransientStore;
042import org.nuxeo.ecm.core.transientstore.api.TransientStoreService;
043import org.nuxeo.ecm.platform.oauth2.OAuth2Error;
044import org.nuxeo.ecm.platform.oauth2.clients.OAuth2Client;
045import org.nuxeo.ecm.platform.oauth2.clients.OAuth2ClientService;
046import org.nuxeo.runtime.api.Framework;
047
048/**
049 * @author <a href="mailto:ak@nuxeo.com">Arnaud Kervern</a>
050 * @since 5.9.2
051 */
052public class AuthorizationRequest extends OAuth2Request {
053
054    private static final Log log = LogFactory.getLog(AuthorizationRequest.class);
055
056    public static final String MISSING_REQUIRED_FIELD_MESSAGE = "Missing required field \"%s\".";
057
058    public static final String STORE_NAME = "authorizationRequestStore";
059
060    protected String responseType;
061
062    protected String scope;
063
064    protected Date creationDate;
065
066    protected String authorizationCode;
067
068    protected String authorizationKey;
069
070    protected String username;
071
072    public static AuthorizationRequest fromRequest(HttpServletRequest request) {
073        return new AuthorizationRequest(request);
074    }
075
076    public static AuthorizationRequest fromMap(Map<String, Serializable> map) {
077        return new AuthorizationRequest(map);
078    }
079
080    public static void store(String key, AuthorizationRequest authorizationRequest) {
081        TransientStoreService transientStoreService = Framework.getService(TransientStoreService.class);
082        TransientStore store = transientStoreService.getStore(STORE_NAME);
083        store.putParameters(key, authorizationRequest.toMap());
084    }
085
086    public static AuthorizationRequest get(String key) {
087        TransientStoreService transientStoreService = Framework.getService(TransientStoreService.class);
088        TransientStore store = transientStoreService.getStore(STORE_NAME);
089        Map<String, Serializable> parameters = store.getParameters(key);
090        if (parameters != null) {
091            AuthorizationRequest authorizationRequest = AuthorizationRequest.fromMap(parameters);
092            return authorizationRequest.isExpired() ? null : authorizationRequest;
093        }
094        return null;
095    }
096
097    public static void remove(String key) {
098        TransientStoreService transientStoreService = Framework.getService(TransientStoreService.class);
099        TransientStore store = transientStoreService.getStore(STORE_NAME);
100        store.remove(key);
101    }
102
103    protected AuthorizationRequest(HttpServletRequest request) {
104        super(request);
105        responseType = request.getParameter(RESPONSE_TYPE_PARAM);
106        scope = request.getParameter(SCOPE_PARAM);
107
108        Principal principal = request.getUserPrincipal();
109        if (principal != null) {
110            username = principal.getName();
111        }
112
113        creationDate = new Date();
114        authorizationKey = RandomStringUtils.random(6, true, false);
115    }
116
117    protected AuthorizationRequest(Map<String, Serializable> map) {
118        clientId = (String) map.get("clientId");
119        redirectURI = (String) map.get("redirectURI");
120        responseType = (String) map.get("responseType");
121        scope = (String) map.get("scope");
122        creationDate = (Date) map.get("creationDate");
123        authorizationCode = (String) map.get("authorizationCode");
124        authorizationKey = (String) map.get("authorizationKey");
125        username = (String) map.get("username");
126    }
127
128    public OAuth2Error checkError() {
129        // Check mandatory fields
130        if (StringUtils.isBlank(clientId)) {
131            return OAuth2Error.invalidRequest(String.format(MISSING_REQUIRED_FIELD_MESSAGE, CLIENT_ID_PARAM));
132        }
133        if (StringUtils.isBlank(responseType)) {
134            return OAuth2Error.invalidRequest(String.format(MISSING_REQUIRED_FIELD_MESSAGE, RESPONSE_TYPE_PARAM));
135        }
136        // Check response type
137        if (!CODE_RESPONSE_TYPE.equals(responseType)) {
138            return OAuth2Error.unsupportedResponseType(String.format("Unknown %s: got \"%s\", expecting \"%s\".",
139                    RESPONSE_TYPE_PARAM, responseType, CODE_RESPONSE_TYPE));
140        }
141
142        // Check if client exists
143        OAuth2ClientService clientService = Framework.getService(OAuth2ClientService.class);
144        OAuth2Client client = clientService.getClient(clientId);
145        if (client == null || !client.isEnabled()) {
146            return OAuth2Error.unauthorizedClient(String.format("Invalid %s: %s.", CLIENT_ID_PARAM, clientId));
147        }
148
149        String clientName = client.getName();
150        if (StringUtils.isBlank(clientName)) {
151            log.error(String.format(
152                    "No name set for OAuth2 client %s. It is a required field, please make sure you update this OAuth2 client.",
153                    client));
154            // Here we are just checking that the client has a name since it is now a required field but it might be
155            // empty for an old client.
156            // Yet we don't return an error for backward compatibility since an empty name is not a security issue and
157            // should not prevent the authorization request from working.
158        }
159
160        List<String> clientRedirectURIs = client.getRedirectURIs();
161        if (CollectionUtils.isEmpty(clientRedirectURIs)) {
162            log.error(String.format(
163                    "No redirect URI set for OAuth2 client %s, at least one is required. Please make sure you update this OAuth2 client.",
164                    client));
165            // Checking that the client has at least one redirect URI since it is now a required field but it might be
166            // empty for an old client.
167            // In this case we return an error since we cannot trust the redirect_uri parameter for security reasons.
168            return OAuth2Error.invalidRequest("No redirect URI configured for the app.");
169        }
170
171        String clientRedirectURI = null;
172        // No redirect_uri parameter, use the first redirect URI registered for this client
173        if (StringUtils.isBlank(redirectURI)) {
174            clientRedirectURI = clientRedirectURIs.get(0);
175        } else {
176            // Check that the redirect_uri parameter matches one of the the redirect URIs registered for this client
177            if (!clientRedirectURIs.contains(redirectURI)) {
178                return OAuth2Error.invalidRequest(String.format(
179                        "Invalid %s parameter: %s. It must exactly match one of the redirect URIs configured for the app.",
180                        REDIRECT_URI_PARAM, redirectURI));
181            }
182            clientRedirectURI = redirectURI;
183        }
184
185        // Check redirect URI validity
186        if (!OAuth2Client.isRedirectURIValid(clientRedirectURI)) {
187            log.error(String.format(
188                    "The redirect URI %s set for OAuth2 client %s is invalid: it must not be empty and start with https for security reasons. Please make sure you update this OAuth2 client.",
189                    clientRedirectURI, client));
190            return OAuth2Error.invalidRequest(String.format(
191                    "Invalid redirect URI configured for the app: %s. It must not be empty and start with https for security reasons.",
192                    clientRedirectURI));
193        }
194
195        return null;
196    }
197
198    public boolean isExpired() {
199        // RFC 4.1.2, Authorization code lifetime is 10
200        return new Date().getTime() - creationDate.getTime() > 10 * 60 * 1000;
201    }
202
203    public String getUsername() {
204        return username;
205    }
206
207    public String getAuthorizationCode() {
208        if (StringUtils.isBlank(authorizationCode)) {
209            authorizationCode = RandomStringUtils.random(10, true, true);
210        }
211        return authorizationCode;
212    }
213
214    public String getAuthorizationKey() {
215        return authorizationKey;
216    }
217
218    public Map<String, Serializable> toMap() {
219        Map<String, Serializable> map = new HashMap<>();
220        if (clientId != null) {
221            map.put("clientId", clientId);
222        }
223        if (redirectURI != null) {
224            map.put("redirectURI", redirectURI);
225        }
226        if (responseType != null) {
227            map.put("responseType", responseType);
228        }
229        if (scope != null) {
230            map.put("scope", scope);
231        }
232        if (creationDate != null) {
233            map.put("creationDate", creationDate);
234        }
235        if (authorizationCode != null) {
236            map.put("authorizationCode", authorizationCode);
237        }
238        if (authorizationKey != null) {
239            map.put("authorizationKey", authorizationKey);
240        }
241        if (username != null) {
242            map.put("username", username);
243        }
244        return map;
245    }
246
247}