View Javadoc
1   /*
2    * Copyright 2002-2011 the original author or authors.
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  package org.springframework.security.oauth2.client.http;
17  
18  import org.springframework.http.HttpHeaders;
19  import org.springframework.http.HttpStatus;
20  import org.springframework.http.client.ClientHttpResponse;
21  import org.springframework.http.converter.HttpMessageConversionException;
22  import org.springframework.http.converter.HttpMessageConverter;
23  import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
24  import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
25  import org.springframework.security.oauth2.common.OAuth2AccessToken;
26  import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
27  import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
28  import org.springframework.security.oauth2.common.exceptions.UserDeniedAuthorizationException;
29  import org.springframework.util.FileCopyUtils;
30  import org.springframework.web.client.*;
31  
32  import java.io.ByteArrayInputStream;
33  import java.io.IOException;
34  import java.io.InputStream;
35  import java.util.List;
36  import java.util.Map;
37  
38  /**
39   * Error handler specifically for an oauth 2 response.
40   * @author Ryan Heaton
41   */
42  public class OAuth2ErrorHandler implements ResponseErrorHandler {
43  
44  	private final ResponseErrorHandler errorHandler;
45  
46  	private final OAuth2ProtectedResourceDetails resource;
47  
48  	private List<HttpMessageConverter<?>> messageConverters = new RestTemplate().getMessageConverters();
49  
50  	/**
51  	 * Construct an error handler that can deal with OAuth2 concerns before handling the error in the default fashion.
52  	 */
53  	public OAuth2ErrorHandler(OAuth2ProtectedResourceDetails resource) {
54  		this.resource = resource;
55  		this.errorHandler = new DefaultResponseErrorHandler();
56  	}
57  
58  	/**
59  	 * @param messageConverters the messageConverters to set
60  	 */
61  	public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
62  		this.messageConverters = messageConverters;
63  	}
64  
65  	/**
66  	 * Construct an error handler that can deal with OAuth2 concerns before delegating to acustom handler.
67  	 * 
68  	 * @param errorHandler a delegate handler
69  	 */
70  	public OAuth2ErrorHandler(ResponseErrorHandler errorHandler, OAuth2ProtectedResourceDetails resource) {
71  		this.resource = resource;
72  		this.errorHandler = errorHandler;
73  	}
74  
75  	public boolean hasError(ClientHttpResponse response) throws IOException {
76  		return HttpStatus.Series.CLIENT_ERROR.equals(response.getStatusCode().series())
77  				|| this.errorHandler.hasError(response);
78  	}
79  
80  	public void handleError(final ClientHttpResponse response) throws IOException {
81  		if (!HttpStatus.Series.CLIENT_ERROR.equals(response.getStatusCode().series())) {
82  			// We should only care about 400 level errors. Ex: A 500 server error shouldn't
83  			// be an oauth related error.
84  			errorHandler.handleError(response);
85  		}
86  		else {
87  			// Need to use buffered response because input stream may need to be consumed multiple times.
88  			ClientHttpResponse bufferedResponse = new ClientHttpResponse() {
89  				private byte[] lazyBody;
90  
91  				public HttpStatus getStatusCode() throws IOException {
92  					return response.getStatusCode();
93  				}
94  
95  				public synchronized InputStream getBody() throws IOException {
96  					if (lazyBody == null) {
97  						InputStream bodyStream = response.getBody();
98  						if (bodyStream != null) {
99  							lazyBody = FileCopyUtils.copyToByteArray(bodyStream);
100 						}
101 						else {
102 							lazyBody = new byte[0];
103 						}
104 					}
105 					return new ByteArrayInputStream(lazyBody);
106 				}
107 
108 				public HttpHeaders getHeaders() {
109 					return response.getHeaders();
110 				}
111 
112 				public String getStatusText() throws IOException {
113 					return response.getStatusText();
114 				}
115 
116 				public void close() {
117 					response.close();
118 				}
119 
120 				public int getRawStatusCode() throws IOException {
121 					return this.getStatusCode().value();
122 				}
123 			};
124 
125 			try {
126 				HttpMessageConverterExtractor<OAuth2Exception> extractor = new HttpMessageConverterExtractor<OAuth2Exception>(
127 						OAuth2Exception.class, messageConverters);
128 				try {
129 					OAuth2Exception oauth2Exception = extractor.extractData(bufferedResponse);
130 					if (oauth2Exception != null) {
131 						// gh-875
132 						if (oauth2Exception.getClass() == UserDeniedAuthorizationException.class &&
133 								bufferedResponse.getStatusCode().equals(HttpStatus.FORBIDDEN)) {
134 							oauth2Exception = new OAuth2AccessDeniedException(oauth2Exception.getMessage());
135 						}
136 						// If we can get an OAuth2Exception, it is likely to have more information
137 						// than the header does, so just re-throw it here.
138 						throw oauth2Exception;
139 					}
140 				}
141 				catch (RestClientException e) {
142 					// ignore
143 				}
144 				catch (HttpMessageConversionException e){
145 					// ignore
146 				}
147 
148 				// first try: www-authenticate error
149 				List<String> authenticateHeaders = bufferedResponse.getHeaders().get("WWW-Authenticate");
150 				if (authenticateHeaders != null) {
151 					for (String authenticateHeader : authenticateHeaders) {
152 						maybeThrowExceptionFromHeader(authenticateHeader, OAuth2AccessToken.BEARER_TYPE);
153 						maybeThrowExceptionFromHeader(authenticateHeader, OAuth2AccessToken.OAUTH2_TYPE);
154 					}
155 				}
156 
157 				// then delegate to the custom handler
158 				errorHandler.handleError(bufferedResponse);
159 			}
160 			catch (InvalidTokenException ex) {
161 				// Special case: an invalid token can be renewed so tell the caller what to do
162 				throw new AccessTokenRequiredException(resource);
163 			}
164 			catch (OAuth2Exception ex) {
165 				if (!ex.getClass().equals(OAuth2Exception.class)) {
166 					// There is more information here than the caller would get from an HttpClientErrorException so
167 					// rethrow
168 					throw ex;
169 				}
170 				// This is not an exception that is really understood, so allow our delegate
171 				// to handle it in a non-oauth way
172 				errorHandler.handleError(bufferedResponse);
173 			}
174 		}
175 	}
176 
177 	private void maybeThrowExceptionFromHeader(String authenticateHeader, String headerType) {
178 		headerType = headerType.toLowerCase();
179 		if (authenticateHeader.toLowerCase().startsWith(headerType)) {
180 			Map<String, String> headerEntries = StringSplitUtils.splitEachArrayElementAndCreateMap(
181 					StringSplitUtils.splitIgnoringQuotes(authenticateHeader.substring(headerType.length()), ','), "=",
182 					"\"");
183 			OAuth2Exception ex = OAuth2Exception.valueOf(headerEntries);
184 			if (ex instanceof InvalidTokenException) {
185 				// Special case: an invalid token can be renewed so tell the caller what to do
186 				throw new AccessTokenRequiredException(resource);
187 			}
188 			throw ex;
189 		}
190 	}
191 
192 }