View Javadoc
1   /*
2    * Copyright 2008 Web Cohesion
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.security.oauth.provider.filter;
18  
19  import org.apache.commons.logging.Log;
20  import org.apache.commons.logging.LogFactory;
21  import org.springframework.beans.factory.InitializingBean;
22  import org.springframework.beans.factory.annotation.Autowired;
23  import org.springframework.context.MessageSource;
24  import org.springframework.context.MessageSourceAware;
25  import org.springframework.context.support.MessageSourceAccessor;
26  import org.springframework.security.core.Authentication;
27  import org.springframework.security.core.AuthenticationException;
28  import org.springframework.security.core.SpringSecurityMessageSource;
29  import org.springframework.security.core.context.SecurityContextHolder;
30  import org.springframework.security.oauth.common.OAuthConsumerParameter;
31  import org.springframework.security.oauth.common.OAuthException;
32  import org.springframework.security.oauth.common.signature.*;
33  import org.springframework.security.oauth.provider.ConsumerAuthentication;
34  import org.springframework.security.oauth.provider.ConsumerCredentials;
35  import org.springframework.security.oauth.provider.ConsumerDetails;
36  import org.springframework.security.oauth.provider.ConsumerDetailsService;
37  import org.springframework.security.oauth.provider.InvalidOAuthParametersException;
38  import org.springframework.security.oauth.provider.OAuthAuthenticationDetails;
39  import org.springframework.security.oauth.provider.OAuthProcessingFilterEntryPoint;
40  import org.springframework.security.oauth.provider.OAuthProviderSupport;
41  import org.springframework.security.oauth.provider.OAuthVersionUnsupportedException;
42  import org.springframework.security.oauth.provider.nonce.ExpiringTimestampNonceServices;
43  import org.springframework.security.oauth.provider.nonce.OAuthNonceServices;
44  import org.springframework.security.oauth.provider.token.OAuthProviderToken;
45  import org.springframework.security.oauth.provider.token.OAuthProviderTokenServices;
46  import org.springframework.util.Assert;
47  
48  import javax.servlet.*;
49  import javax.servlet.http.HttpServletRequest;
50  import javax.servlet.http.HttpServletResponse;
51  import java.io.IOException;
52  import java.util.ArrayList;
53  import java.util.Arrays;
54  import java.util.List;
55  import java.util.Map;
56  
57  /**
58   * OAuth processing filter. This filter should be applied to requests for OAuth protected resources (see OAuth Core 1.0).
59   *
60   * @author Ryan Heaton
61   */
62  public abstract class OAuthProviderProcessingFilter implements Filter, InitializingBean, MessageSourceAware {
63  
64    /**
65     * Attribute for indicating that OAuth processing has already occurred.
66     */
67    public static final String OAUTH_PROCESSING_HANDLED = "org.springframework.security.oauth.provider.OAuthProviderProcessingFilter#SKIP_PROCESSING";
68  
69    private final Log log = LogFactory.getLog(getClass());
70    private final List<String> allowedMethods = new ArrayList<String>(Arrays.asList("GET", "POST"));
71    private OAuthProcessingFilterEntryPointEntryPoint.html#OAuthProcessingFilterEntryPoint">OAuthProcessingFilterEntryPoint authenticationEntryPoint = new OAuthProcessingFilterEntryPoint();
72    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
73    private String filterProcessesUrl = "/oauth_filter";
74    private OAuthProviderSupport providerSupport = new CoreOAuthProviderSupport();
75    private OAuthSignatureMethodFactory signatureMethodFactory = new CoreOAuthSignatureMethodFactory();
76    private OAuthNonceServices nonceServices = new ExpiringTimestampNonceServices();
77    private boolean ignoreMissingCredentials = false;
78    private OAuthProviderTokenServices tokenServices;
79  
80    private ConsumerDetailsService consumerDetailsService;
81  
82    public void afterPropertiesSet() throws Exception {
83      Assert.notNull(consumerDetailsService, "A consumer details service is required.");
84      Assert.notNull(tokenServices, "Token services are required.");
85    }
86  
87    public void init(FilterConfig ignored) throws ServletException {
88    }
89  
90    public void destroy() {
91    }
92  
93    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
94      HttpServletRequest request = (HttpServletRequest) servletRequest;
95      HttpServletResponse response = (HttpServletResponse) servletResponse;
96  
97      if (!skipProcessing(request)) {
98        if (requiresAuthentication(request, response, chain)) {
99          if (!allowMethod(request.getMethod().toUpperCase())) {
100           if (log.isDebugEnabled()) {
101             log.debug("Method " + request.getMethod() + " not supported.");
102           }
103 
104           response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
105           return;
106         }
107 
108         try {
109           Map<String, String> oauthParams = getProviderSupport().parseParameters(request);
110 
111           if (parametersAreAdequate(oauthParams)) {
112 
113             if (log.isDebugEnabled()) {
114               StringBuilder builder = new StringBuilder("OAuth parameters parsed: ");
115               for (String param : oauthParams.keySet()) {
116                 builder.append(param).append('=').append(oauthParams.get(param)).append(' ');
117               }
118               log.debug(builder.toString());
119             }
120 
121             String consumerKey = oauthParams.get(OAuthConsumerParameter.oauth_consumer_key.toString());
122             if (consumerKey == null) {
123               throw new InvalidOAuthParametersException(messages.getMessage("OAuthProcessingFilter.missingConsumerKey", "Missing consumer key."));
124             }
125 
126             //load the consumer details.
127             ConsumerDetails consumerDetails = getConsumerDetailsService().loadConsumerByConsumerKey(consumerKey);
128             if (log.isDebugEnabled()) {
129               log.debug("Consumer details loaded for " + consumerKey + ": " + consumerDetails);
130             }
131 
132             //validate the parameters for the consumer.
133             validateOAuthParams(consumerDetails, oauthParams);
134             if (log.isDebugEnabled()) {
135               log.debug("Parameters validated.");
136             }
137 
138             //extract the credentials.
139             String token = oauthParams.get(OAuthConsumerParameter.oauth_token.toString());
140             String signatureMethod = oauthParams.get(OAuthConsumerParameter.oauth_signature_method.toString());
141             String signature = oauthParams.get(OAuthConsumerParameter.oauth_signature.toString());
142             String signatureBaseString = getProviderSupport().getSignatureBaseString(request);
143             ConsumerCredentialsder/ConsumerCredentials.html#ConsumerCredentials">ConsumerCredentials credentials = new ConsumerCredentials(consumerKey, signature, signatureMethod, signatureBaseString, token);
144 
145             //create an authentication request.
146             ConsumerAuthenticationnsumerAuthentication.html#ConsumerAuthentication">ConsumerAuthentication authentication = new ConsumerAuthentication(consumerDetails, credentials, oauthParams);
147             authentication.setDetails(createDetails(request, consumerDetails));
148 
149             Authentication previousAuthentication = SecurityContextHolder.getContext().getAuthentication();
150             try {
151               //set the authentication request (unauthenticated) into the context.
152               SecurityContextHolder.getContext().setAuthentication(authentication);
153 
154               //validate the signature.
155               validateSignature(authentication);
156 
157               //mark the authentication request as validated.
158               authentication.setSignatureValidated(true);
159 
160               //mark that processing has been handled.
161               request.setAttribute(OAUTH_PROCESSING_HANDLED, Boolean.TRUE);
162 
163               if (log.isDebugEnabled()) {
164                 log.debug("Signature validated.");
165               }
166 
167               //go.
168               onValidSignature(request, response, chain);
169             }
170             finally {
171               //clear out the consumer authentication to make sure it doesn't get cached.
172               resetPreviousAuthentication(previousAuthentication);
173             }
174           }
175           else if (!isIgnoreInadequateCredentials()) {
176             throw new InvalidOAuthParametersException(messages.getMessage("OAuthProcessingFilter.missingCredentials", "Inadequate OAuth consumer credentials."));
177           }
178           else {
179             if (log.isDebugEnabled()) {
180               log.debug("Supplied OAuth parameters are inadequate. Ignoring.");
181             }
182             chain.doFilter(request, response);
183           }
184         }
185         catch (AuthenticationException ae) {
186           fail(request, response, ae);
187         }
188         catch (ServletException e) {
189           if (e.getRootCause() instanceof AuthenticationException) {
190             fail(request, response, (AuthenticationException) e.getRootCause());
191           }
192           else {
193             throw e;
194           }
195         }
196       }
197       else {
198         if (log.isDebugEnabled()) {
199           log.debug("Request does not require authentication.  OAuth processing skipped.");
200         }
201 
202         chain.doFilter(servletRequest, servletResponse);
203       }
204     }
205     else {
206       if (log.isDebugEnabled()) {
207         log.debug("Processing explicitly skipped.");
208       }
209 
210       chain.doFilter(servletRequest, servletResponse);
211     }
212   }
213 
214   /**
215    * By default, OAuth parameters are adequate if a consumer key is present.
216    *
217    * @param oauthParams The oauth params.
218    * @return Whether the parsed parameters are adequate.
219    */
220   protected boolean parametersAreAdequate(Map<String, String> oauthParams) {
221     return oauthParams.containsKey(OAuthConsumerParameter.oauth_consumer_key.toString());
222   }
223 
224   protected void resetPreviousAuthentication(Authentication previousAuthentication) {
225     SecurityContextHolder.getContext().setAuthentication(previousAuthentication);
226   }
227 
228   /**
229    * Create the details for the authentication request.
230    *
231    * @param request The request.
232    * @param consumerDetails The consumer details.
233    * @return The authentication details.
234    */
235   protected Object createDetails(HttpServletRequest request, ConsumerDetails consumerDetails) {
236     return new OAuthAuthenticationDetails(request, consumerDetails);
237   }
238 
239   /**
240    * Whether to allow the specified HTTP method.
241    *
242    * @param method The HTTP method to check for allowing.
243    * @return Whether to allow the specified method.
244    */
245   protected boolean allowMethod(String method) {
246     return allowedMethods.contains(method);
247   }
248 
249   /**
250    * Validate the signature of the request given the authentication request.
251    *
252    * @param authentication The authentication request.
253    */
254   protected void validateSignature(ConsumerAuthentication authentication) throws AuthenticationException {
255     SignatureSecret secret = authentication.getConsumerDetails().getSignatureSecret();
256     String token = authentication.getConsumerCredentials().getToken();
257     OAuthProviderToken authToken = null;
258     if (token != null && !"".equals(token)) {
259       authToken = getTokenServices().getToken(token);
260     }
261 
262     String signatureMethod = authentication.getConsumerCredentials().getSignatureMethod();
263     OAuthSignatureMethod method;
264     try {
265       method = getSignatureMethodFactory().getSignatureMethod(signatureMethod, secret, authToken != null ? authToken.getSecret() : null);
266     }
267     catch (UnsupportedSignatureMethodException e) {
268       throw new OAuthException(e.getMessage(), e);
269     }
270 
271     String signatureBaseString = authentication.getConsumerCredentials().getSignatureBaseString();
272     String signature = authentication.getConsumerCredentials().getSignature();
273     if (log.isDebugEnabled()) {
274       log.debug("Verifying signature " + signature + " for signature base string " + signatureBaseString + " with method " + method.getName() + ".");
275     }
276     method.verify(signatureBaseString, signature);
277   }
278 
279   /**
280    * Logic executed on valid signature. The security context can be assumed to hold a verified, authenticated
281    * {@link org.springframework.security.oauth.provider.ConsumerAuthentication}
282    *
283    * Default implementation continues the chain.
284    *
285    * @param request  The request.
286    * @param response The response
287    * @param chain    The filter chain.
288    */
289   protected abstract void onValidSignature(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException;
290 
291   /**
292    * Validates the OAuth parameters for the given consumer. Base implementation validates only those parameters
293    * that are required for all OAuth requests. This includes the nonce and timestamp, but not the signature.
294    *
295    * @param consumerDetails The consumer details.
296    * @param oauthParams     The OAuth parameters to validate.
297    * @throws InvalidOAuthParametersException If the OAuth parameters are invalid.
298    */
299   protected void validateOAuthParams(ConsumerDetails consumerDetails, Map<String, String> oauthParams) throws InvalidOAuthParametersException {
300     String version = oauthParams.get(OAuthConsumerParameter.oauth_version.toString());
301     if ((version != null) && (!"1.0".equals(version))) {
302       throw new OAuthVersionUnsupportedException("Unsupported OAuth version: " + version);
303     }
304 
305     String realm = oauthParams.get("realm");
306     realm = realm == null || "".equals(realm) ? null : realm;
307     if ((realm != null) && (!realm.equals(this.authenticationEntryPoint.getRealmName()))) {
308       throw new InvalidOAuthParametersException(messages.getMessage("OAuthProcessingFilter.incorrectRealm",
309                                                                     new Object[]{realm, this.getAuthenticationEntryPoint().getRealmName()},
310                                                                     "Response realm name '{0}' does not match system realm name of '{1}'"));
311     }
312 
313     String signatureMethod = oauthParams.get(OAuthConsumerParameter.oauth_signature_method.toString());
314     if (signatureMethod == null) {
315       throw new InvalidOAuthParametersException(messages.getMessage("OAuthProcessingFilter.missingSignatureMethod", "Missing signature method."));
316     }
317 
318     String signature = oauthParams.get(OAuthConsumerParameter.oauth_signature.toString());
319     if (signature == null) {
320       throw new InvalidOAuthParametersException(messages.getMessage("OAuthProcessingFilter.missingSignature", "Missing signature."));
321     }
322 
323     String timestamp = oauthParams.get(OAuthConsumerParameter.oauth_timestamp.toString());
324     if (timestamp == null) {
325       throw new InvalidOAuthParametersException(messages.getMessage("OAuthProcessingFilter.missingTimestamp", "Missing timestamp."));
326     }
327 
328     String nonce = oauthParams.get(OAuthConsumerParameter.oauth_nonce.toString());
329     if (nonce == null) {
330       throw new InvalidOAuthParametersException(messages.getMessage("OAuthProcessingFilter.missingNonce", "Missing nonce."));
331     }
332 
333     try {
334       getNonceServices().validateNonce(consumerDetails, Long.parseLong(timestamp), nonce);
335     }
336     catch (NumberFormatException e) {
337       throw new InvalidOAuthParametersException(messages.getMessage("OAuthProcessingFilter.invalidTimestamp", new Object[]{timestamp}, "Timestamp must be a positive integer. Invalid value: {0}"));
338     }
339 
340     validateAdditionalParameters(consumerDetails, oauthParams);
341   }
342 
343   /**
344    * Do any additional validation checks for the specified oauth params.  Default implementation is a no-op.
345    *
346    * @param consumerDetails The consumer details.
347    * @param oauthParams The params.
348    */
349   protected void validateAdditionalParameters(ConsumerDetails consumerDetails, Map<String, String> oauthParams) {
350   }
351 
352   /**
353    * Logic to be performed on a new timestamp.  The default behavior expects that the timestamp should not be new.
354    *
355    * @throws org.springframework.security.core.AuthenticationException
356    *          If the timestamp shouldn't be new.
357    */
358   protected void onNewTimestamp() throws AuthenticationException {
359     throw new InvalidOAuthParametersException(messages.getMessage("OAuthProcessingFilter.timestampNotNew", "A new timestamp should not be used in a request for an access token."));
360   }
361 
362   /**
363    * Common logic for OAuth failed.
364    *
365    * @param request  The request.
366    * @param response The response.
367    * @param failure  The failure.
368    * @throws IOException thrown when there's an underlying IO exception
369    * @throws ServletException thrown in the case of an underlying Servlet exception 
370    */
371   protected void fail(HttpServletRequest request, HttpServletResponse response, AuthenticationException failure) throws IOException, ServletException {
372     SecurityContextHolder.getContext().setAuthentication(null);
373 
374     if (log.isDebugEnabled()) {
375       log.debug(failure);
376     }
377 
378     authenticationEntryPoint.commence(request, response, failure);
379   }
380 
381   /**
382    * Whether this filter is configured to process the specified request.
383    *
384    * @param request     The request.
385    * @param response    The response
386    * @param filterChain The filter chain
387    * @return Whether this filter is configured to process the specified request.
388    */
389   protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
390     //copied from org.springframework.security.ui.AbstractProcessingFilter.requiresAuthentication
391     String uri = request.getRequestURI();
392     int pathParamIndex = uri.indexOf(';');
393 
394     if (pathParamIndex > 0) {
395       // strip everything after the first semi-colon
396       uri = uri.substring(0, pathParamIndex);
397     }
398 
399     if ("".equals(request.getContextPath())) {
400       return uri.endsWith(filterProcessesUrl);
401     }
402 
403     boolean match = uri.endsWith(request.getContextPath() + filterProcessesUrl);
404     if (log.isDebugEnabled()) {
405       log.debug(uri + (match ? " matches " : " does not match ") + filterProcessesUrl);
406     }
407     return match;
408   }
409 
410   /**
411    * Whether to skip processing for the specified request.
412    *
413    * @param request The request.
414    * @return Whether to skip processing.
415    */
416   protected boolean skipProcessing(HttpServletRequest request) {
417     return ((request.getAttribute(OAUTH_PROCESSING_HANDLED) != null) && (Boolean.TRUE.equals(request.getAttribute(OAUTH_PROCESSING_HANDLED))));
418   }
419 
420   /**
421    * The authentication entry point.
422    *
423    * @return The authentication entry point.
424    */
425   public OAuthProcessingFilterEntryPoint getAuthenticationEntryPoint() {
426     return authenticationEntryPoint;
427   }
428 
429   /**
430    * The authentication entry point.
431    *
432    * @param authenticationEntryPoint The authentication entry point.
433    */
434   @Autowired (required = false)
435   public void setAuthenticationEntryPoint(OAuthProcessingFilterEntryPoint authenticationEntryPoint) {
436     this.authenticationEntryPoint = authenticationEntryPoint;
437   }
438 
439   /**
440    * The consumer details service.
441    *
442    * @return The consumer details service.
443    */
444   public ConsumerDetailsService getConsumerDetailsService() {
445     return consumerDetailsService;
446   }
447 
448   /**
449    * The consumer details service.
450    *
451    * @param consumerDetailsService The consumer details service.
452    */
453   @Autowired
454   public void setConsumerDetailsService(ConsumerDetailsService consumerDetailsService) {
455     this.consumerDetailsService = consumerDetailsService;
456   }
457 
458   /**
459    * The nonce services.
460    *
461    * @return The nonce services.
462    */
463   public OAuthNonceServices getNonceServices() {
464     return nonceServices;
465   }
466 
467   /**
468    * The nonce services.
469    *
470    * @param nonceServices The nonce services.
471    */
472   @Autowired (required = false)
473   public void setNonceServices(OAuthNonceServices nonceServices) {
474     this.nonceServices = nonceServices;
475   }
476 
477   /**
478    * Get the OAuth token services.
479    *
480    * @return The OAuth token services.
481    */
482   public OAuthProviderTokenServices getTokenServices() {
483     return tokenServices;
484   }
485 
486   /**
487    * The OAuth token services.
488    *
489    * @param tokenServices The OAuth token services.
490    */
491   @Autowired
492   public void setTokenServices(OAuthProviderTokenServices tokenServices) {
493     this.tokenServices = tokenServices;
494   }
495 
496   /**
497    * The URL for which this filter will be applied.
498    *
499    * @return The URL for which this filter will be applied.
500    */
501   public String getFilterProcessesUrl() {
502     return filterProcessesUrl;
503   }
504 
505   /**
506    * The URL for which this filter will be applied.
507    *
508    * @param filterProcessesUrl The URL for which this filter will be applied.
509    */
510   public void setFilterProcessesUrl(String filterProcessesUrl) {
511     this.filterProcessesUrl = filterProcessesUrl;
512   }
513 
514   /**
515    * Set the message source.
516    *
517    * @param messageSource The message source.
518    */
519   public void setMessageSource(MessageSource messageSource) {
520     this.messages = new MessageSourceAccessor(messageSource);
521   }
522 
523   /**
524    * The OAuth provider support.
525    *
526    * @return The OAuth provider support.
527    */
528   public OAuthProviderSupport getProviderSupport() {
529     return providerSupport;
530   }
531 
532   /**
533    * The OAuth provider support.
534    *
535    * @param providerSupport The OAuth provider support.
536    */
537   @Autowired (required = false)
538   public void setProviderSupport(OAuthProviderSupport providerSupport) {
539     this.providerSupport = providerSupport;
540   }
541 
542   /**
543    * The OAuth signature method factory.
544    *
545    * @return The OAuth signature method factory.
546    */
547   public OAuthSignatureMethodFactory getSignatureMethodFactory() {
548     return signatureMethodFactory;
549   }
550 
551   /**
552    * The OAuth signature method factory.
553    *
554    * @param signatureMethodFactory The OAuth signature method factory.
555    */
556   @Autowired (required = false)
557   public void setSignatureMethodFactory(OAuthSignatureMethodFactory signatureMethodFactory) {
558     this.signatureMethodFactory = signatureMethodFactory;
559   }
560 
561   /**
562    * Whether to ignore missing OAuth credentials.
563    *
564    * @return Whether to ignore missing OAuth credentials.
565    */
566   public boolean isIgnoreInadequateCredentials() {
567     return ignoreMissingCredentials;
568   }
569 
570   /**
571    * Whether to ignore missing OAuth credentials.
572    *
573    * @param ignoreMissingCredentials Whether to ignore missing OAuth credentials.
574    */
575   public void setIgnoreMissingCredentials(boolean ignoreMissingCredentials) {
576     this.ignoreMissingCredentials = ignoreMissingCredentials;
577   }
578 
579   /**
580    * The allowed set of HTTP methods.
581    *
582    * @param allowedMethods The allowed set of methods.
583    */
584   public void setAllowedMethods(List<String> allowedMethods) {
585     this.allowedMethods.clear();
586     if (allowedMethods != null) {
587       for (String allowedMethod : allowedMethods) {
588         this.allowedMethods.add(allowedMethod.toUpperCase());
589       }
590     }
591   }
592 }