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}