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  
17  package org.springframework.security.oauth2.provider.endpoint;
18  
19  import org.springframework.http.HttpHeaders;
20  import org.springframework.http.HttpMethod;
21  import org.springframework.http.HttpStatus;
22  import org.springframework.http.ResponseEntity;
23  import org.springframework.security.authentication.InsufficientAuthenticationException;
24  import org.springframework.security.core.Authentication;
25  import org.springframework.security.oauth2.common.OAuth2AccessToken;
26  import org.springframework.security.oauth2.common.exceptions.BadClientCredentialsException;
27  import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
28  import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
29  import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
30  import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
31  import org.springframework.security.oauth2.common.exceptions.UnsupportedGrantTypeException;
32  import org.springframework.security.oauth2.common.util.OAuth2Utils;
33  import org.springframework.security.oauth2.provider.ClientDetails;
34  import org.springframework.security.oauth2.provider.ClientRegistrationException;
35  import org.springframework.security.oauth2.provider.OAuth2Authentication;
36  import org.springframework.security.oauth2.provider.OAuth2RequestValidator;
37  import org.springframework.security.oauth2.provider.TokenRequest;
38  import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestValidator;
39  import org.springframework.util.StringUtils;
40  import org.springframework.web.HttpRequestMethodNotSupportedException;
41  import org.springframework.web.bind.annotation.ExceptionHandler;
42  import org.springframework.web.bind.annotation.RequestMapping;
43  import org.springframework.web.bind.annotation.RequestMethod;
44  import org.springframework.web.bind.annotation.RequestParam;
45  
46  import java.security.Principal;
47  import java.util.Arrays;
48  import java.util.Collections;
49  import java.util.HashSet;
50  import java.util.Map;
51  import java.util.Set;
52  
53  /**
54   * <p>
55   * Endpoint for token requests as described in the OAuth2 spec. Clients post requests with a <code>grant_type</code>
56   * parameter (e.g. "authorization_code") and other parameters as determined by the grant type. Supported grant types are
57   * handled by the provided {@link #setTokenGranter(org.springframework.security.oauth2.provider.TokenGranter) token
58   * granter}.
59   * </p>
60   * 
61   * <p>
62   * Clients must be authenticated using a Spring Security {@link Authentication} to access this endpoint, and the client
63   * id is extracted from the authentication token. The best way to arrange this (as per the OAuth2 spec) is to use HTTP
64   * basic authentication for this endpoint with standard Spring Security support.
65   * </p>
66   * 
67   * @author Dave Syer
68   * 
69   */
70  @FrameworkEndpoint
71  public class TokenEndpoint extends AbstractEndpoint {
72  
73  	private OAuth2RequestValidator oAuth2RequestValidator = new DefaultOAuth2RequestValidator();
74  
75  	private Set<HttpMethod> allowedRequestMethods = new HashSet<HttpMethod>(Arrays.asList(HttpMethod.POST));
76  
77  	@RequestMapping(value = "/oauth/token", method=RequestMethod.GET)
78  	public ResponseEntity<OAuth2AccessToken> getAccessToken(Principal principal, @RequestParam
79  	Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
80  		if (!allowedRequestMethods.contains(HttpMethod.GET)) {
81  			throw new HttpRequestMethodNotSupportedException("GET");
82  		}
83  		return postAccessToken(principal, parameters);
84  	}
85  	
86  	@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
87  	public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
88  	Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
89  
90  		if (!(principal instanceof Authentication)) {
91  			throw new InsufficientAuthenticationException(
92  					"There is no client authentication. Try adding an appropriate authentication filter.");
93  		}
94  
95  		String clientId = getClientId(principal);
96  		ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
97  
98  		TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
99  
100 		if (clientId != null && !clientId.equals("")) {
101 			// Only validate the client details if a client authenticated during this
102 			// request.
103 			if (!clientId.equals(tokenRequest.getClientId())) {
104 				// double check to make sure that the client ID in the token request is the same as that in the
105 				// authenticated client
106 				throw new InvalidClientException("Given client ID does not match authenticated client");
107 			}
108 		}
109 		if (authenticatedClient != null) {
110 			oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
111 		}
112 		if (!StringUtils.hasText(tokenRequest.getGrantType())) {
113 			throw new InvalidRequestException("Missing grant type");
114 		}
115 		if (tokenRequest.getGrantType().equals("implicit")) {
116 			throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
117 		}
118 
119 		if (isAuthCodeRequest(parameters)) {
120 			// The scope was requested or determined during the authorization step
121 			if (!tokenRequest.getScope().isEmpty()) {
122 				logger.debug("Clearing scope of incoming token request");
123 				tokenRequest.setScope(Collections.<String> emptySet());
124 			}
125 		}
126 
127 		if (isRefreshTokenRequest(parameters)) {
128 			// A refresh token has its own default scopes, so we should ignore any added by the factory here.
129 			tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
130 		}
131 
132 		OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
133 		if (token == null) {
134 			throw new UnsupportedGrantTypeException("Unsupported grant type");
135 		}
136 
137 		return getResponse(token);
138 
139 	}
140 
141 	/**
142 	 * @param principal the currently authentication principal
143 	 * @return a client id if there is one in the principal
144 	 */
145 	protected String getClientId(Principal principal) {
146 		Authentication client = (Authentication) principal;
147 		if (!client.isAuthenticated()) {
148 			throw new InsufficientAuthenticationException("The client is not authenticated.");
149 		}
150 		String clientId = client.getName();
151 		if (client instanceof OAuth2Authentication) {
152 			// Might be a client and user combined authentication
153 			clientId = ((OAuth2Authentication) client).getOAuth2Request().getClientId();
154 		}
155 		return clientId;
156 	}
157 
158 	@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
159 	public ResponseEntity<OAuth2Exception> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) throws Exception {
160 		if (logger.isInfoEnabled()) {
161 			logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
162 		}
163 	    return getExceptionTranslator().translate(e);
164 	}
165 	
166 	@ExceptionHandler(Exception.class)
167 	public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
168 		if (logger.isErrorEnabled()) {
169 			logger.error("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage(), e);
170 		}
171 		return getExceptionTranslator().translate(e);
172 	}
173 	
174 	@ExceptionHandler(ClientRegistrationException.class)
175 	public ResponseEntity<OAuth2Exception> handleClientRegistrationException(Exception e) throws Exception {
176 		if (logger.isWarnEnabled()) {
177 			logger.warn("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
178 		}
179 		return getExceptionTranslator().translate(new BadClientCredentialsException());
180 	}
181 
182 	@ExceptionHandler(OAuth2Exception.class)
183 	public ResponseEntity<OAuth2Exception> handleException(OAuth2Exception e) throws Exception {
184 		if (logger.isWarnEnabled()) {
185 			logger.warn("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
186 		}
187 		return getExceptionTranslator().translate(e);
188 	}
189 
190 	private ResponseEntity<OAuth2AccessToken> getResponse(OAuth2AccessToken accessToken) {
191 		HttpHeaders headers = new HttpHeaders();
192 		headers.set("Cache-Control", "no-store");
193 		headers.set("Pragma", "no-cache");
194 		headers.set("Content-Type", "application/json;charset=UTF-8");
195 		return new ResponseEntity<OAuth2AccessToken>(accessToken, headers, HttpStatus.OK);
196 	}
197 
198 	private boolean isRefreshTokenRequest(Map<String, String> parameters) {
199 		return "refresh_token".equals(parameters.get("grant_type")) && parameters.get("refresh_token") != null;
200 	}
201 
202 	private boolean isAuthCodeRequest(Map<String, String> parameters) {
203 		return "authorization_code".equals(parameters.get("grant_type")) && parameters.get("code") != null;
204 	}
205 
206 	public void setOAuth2RequestValidator(OAuth2RequestValidator oAuth2RequestValidator) {
207 		this.oAuth2RequestValidator = oAuth2RequestValidator;
208 	}
209 
210 	public void setAllowedRequestMethods(Set<HttpMethod> allowedRequestMethods) {
211 		this.allowedRequestMethods = allowedRequestMethods;
212 	}
213 }