/*
* Copyright 2006-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.springframework.security.oauth2.provider.endpoint;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
import org.springframework.security.oauth2.common.exceptions.InvalidScopeException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.TokenRequest;
import org.springframework.security.oauth2.provider.approval.ApprovalStoreUserApprovalHandler;
import org.springframework.security.oauth2.provider.approval.DefaultUserApprovalHandler;
import org.springframework.security.oauth2.provider.approval.InMemoryApprovalStore;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.web.bind.support.SimpleSessionStatus;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.RedirectView;
/**
* @author Dave Syer
*
*/
public class AuthorizationEndpointTests {
private AuthorizationEndpoint endpoint = new AuthorizationEndpoint();
private HashMap<String, Object> model = new HashMap<String, Object>();
private SimpleSessionStatus sessionStatus = new SimpleSessionStatus();
private UsernamePasswordAuthenticationToken principal = new UsernamePasswordAuthenticationToken("foo", "bar",
Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")));
private BaseClientDetails client;
private AuthorizationRequest getAuthorizationRequest(String clientId, String redirectUri, String state,
String scope, Set<String> responseTypes) {
HashMap<String, String> parameters = new HashMap<String, String>();
parameters.put(OAuth2Utils.CLIENT_ID, clientId);
if (redirectUri != null) {
parameters.put(OAuth2Utils.REDIRECT_URI, redirectUri);
}
if (state != null) {
parameters.put(OAuth2Utils.STATE, state);
}
if (scope != null) {
parameters.put(OAuth2Utils.SCOPE, scope);
}
if (responseTypes != null) {
parameters.put(OAuth2Utils.RESPONSE_TYPE, OAuth2Utils.formatParameterList(responseTypes));
}
return new AuthorizationRequest(parameters, Collections.<String, String> emptyMap(),
parameters.get(OAuth2Utils.CLIENT_ID),
OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)), null, null, false,
parameters.get(OAuth2Utils.STATE), parameters.get(OAuth2Utils.REDIRECT_URI),
OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.RESPONSE_TYPE)));
}
@Before
public void init() throws Exception {
client = new BaseClientDetails();
client.setRegisteredRedirectUri(Collections.singleton("http://anywhere.com"));
client.setAuthorizedGrantTypes(Arrays.asList("authorization_code", "implicit"));
endpoint.setClientDetailsService(new ClientDetailsService() {
public ClientDetails loadClientByClientId(String clientId) throws OAuth2Exception {
return client;
}
});
endpoint.setTokenGranter(new TokenGranter() {
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
return null;
}
});
endpoint.setRedirectResolver(new DefaultRedirectResolver());
endpoint.afterPropertiesSet();
}
@Test(expected = IllegalStateException.class)
public void testMandatoryProperties() throws Exception {
endpoint = new AuthorizationEndpoint();
endpoint.afterPropertiesSet();
}
@Test
public void testStartAuthorizationCodeFlow() throws Exception {
ModelAndView result = endpoint.authorize(model,
getAuthorizationRequest("foo", null, null, "read", Collections.singleton("code"))
.getRequestParameters(), sessionStatus, principal);
assertEquals("forward:/oauth/confirm_access", result.getViewName());
}
@Test
public void testApprovalStoreAddsScopes() throws Exception {
ApprovalStoreUserApprovalHandler userApprovalHandler = new ApprovalStoreUserApprovalHandler();
userApprovalHandler.setApprovalStore(new InMemoryApprovalStore());
endpoint.setUserApprovalHandler(userApprovalHandler);
ModelAndView result = endpoint.authorize(model,
getAuthorizationRequest("foo", null, null, "read", Collections.singleton("code"))
.getRequestParameters(), sessionStatus, principal);
assertEquals("forward:/oauth/confirm_access", result.getViewName());
assertTrue(result.getModel().containsKey("scopes"));
}
@Test(expected = OAuth2Exception.class)
public void testStartAuthorizationCodeFlowForClientCredentialsFails() throws Exception {
client.setAuthorizedGrantTypes(Collections.singleton("client_credentials"));
ModelAndView result = endpoint.authorize(model,
getAuthorizationRequest("foo", null, null, null, Collections.singleton("code")).getRequestParameters(),
sessionStatus, principal);
assertEquals("forward:/oauth/confirm_access", result.getViewName());
}
@Test
public void testAuthorizationCodeWithFragment() throws Exception {
endpoint.setAuthorizationCodeServices(new StubAuthorizationCodeServices());
model.put("authorizationRequest",
getAuthorizationRequest("foo", "http://anywhere.com#bar", null, null, Collections.singleton("code")));
View result = endpoint.approveOrDeny(Collections.singletonMap(OAuth2Utils.USER_OAUTH_APPROVAL, "true"), model,
sessionStatus, principal);
assertEquals("http://anywhere.com?code=thecode#bar", ((RedirectView) result).getUrl());
}
@Test
public void testAuthorizationCodeWithQueryParams() throws Exception {
endpoint.setAuthorizationCodeServices(new StubAuthorizationCodeServices());
model.put(
"authorizationRequest",
getAuthorizationRequest("foo", "http://anywhere.com?foo=bar", null, null, Collections.singleton("code")));
View result = endpoint.approveOrDeny(Collections.singletonMap(OAuth2Utils.USER_OAUTH_APPROVAL, "true"), model,
sessionStatus, principal);
assertEquals("http://anywhere.com?foo=bar&code=thecode", ((RedirectView) result).getUrl());
}
@Test
public void testAuthorizationCodeWithTrickyState() throws Exception {
endpoint.setAuthorizationCodeServices(new StubAuthorizationCodeServices());
model.put("authorizationRequest",
getAuthorizationRequest("foo", "http://anywhere.com", " =?s", null, Collections.singleton("code")));
View result = endpoint.approveOrDeny(Collections.singletonMap(OAuth2Utils.USER_OAUTH_APPROVAL, "true"), model,
sessionStatus, principal);
assertEquals("http://anywhere.com?code=thecode&state=%20%3D?s", ((RedirectView) result).getUrl());
}
@Test
public void testAuthorizationCodeWithMultipleQueryParams() throws Exception {
endpoint.setAuthorizationCodeServices(new StubAuthorizationCodeServices());
model.put(
"authorizationRequest",
getAuthorizationRequest("foo", "http://anywhere.com?foo=bar&bar=foo", null, null,
Collections.singleton("code")));
View result = endpoint.approveOrDeny(Collections.singletonMap(OAuth2Utils.USER_OAUTH_APPROVAL, "true"), model,
sessionStatus, principal);
assertEquals("http://anywhere.com?foo=bar&bar=foo&code=thecode", ((RedirectView) result).getUrl());
}
@Test
public void testAuthorizationCodeWithTrickyQueryParams() throws Exception {
endpoint.setAuthorizationCodeServices(new StubAuthorizationCodeServices());
model.put(
"authorizationRequest",
getAuthorizationRequest("foo", "http://anywhere.com?foo=b =&bar=f $", null, null,
Collections.singleton("code")));
View result = endpoint.approveOrDeny(Collections.singletonMap(OAuth2Utils.USER_OAUTH_APPROVAL, "true"), model,
sessionStatus, principal);
assertEquals("http://anywhere.com?foo=b%20%3D&bar=f%20$&code=thecode", ((RedirectView) result).getUrl());
}
@Test
public void testAuthorizationCodeError() throws Exception {
endpoint.setUserApprovalHandler(new DefaultUserApprovalHandler() {
public AuthorizationRequest checkForPreApproval(AuthorizationRequest authorizationRequest,
Authentication userAuthentication) {
return authorizationRequest;
}
public AuthorizationRequest updateAfterApproval(AuthorizationRequest authorizationRequest,
Authentication userAuthentication) {
return authorizationRequest;
}
public boolean isApproved(AuthorizationRequest authorizationRequest, Authentication userAuthentication) {
return true;
}
});
endpoint.setAuthorizationCodeServices(new StubAuthorizationCodeServices() {
@Override
public String createAuthorizationCode(OAuth2Authentication authentication) {
throw new InvalidScopeException("FOO");
}
});
ModelAndView result = endpoint.authorize(
model,
getAuthorizationRequest("foo", "http://anywhere.com", "mystate", "myscope",
Collections.singleton("code")).getRequestParameters(), sessionStatus, principal);
String url = ((RedirectView) result.getView()).getUrl();
assertTrue("Wrong view: " + result, url.startsWith("http://anywhere.com"));
assertTrue("No error: " + result, url.contains("?error="));
assertTrue("Wrong state: " + result, url.contains("&state=mystate"));
}
@Test
public void testAuthorizationCodeWithMultipleResponseTypes() throws Exception {
Set<String> responseTypes = new HashSet<String>();
responseTypes.add("code");
responseTypes.add("other");
ModelAndView result = endpoint.authorize(model,
getAuthorizationRequest("foo", null, null, "read", responseTypes).getRequestParameters(),
sessionStatus, principal);
assertEquals("forward:/oauth/confirm_access", result.getViewName());
}
@Test
public void testImplicitPreApproved() throws Exception {
endpoint.setTokenGranter(new TokenGranter() {
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken("FOO");
token.setAdditionalInformation(Collections.singletonMap("foo", (Object) "bar"));
return token;
}
});
endpoint.setUserApprovalHandler(new DefaultUserApprovalHandler() {
public AuthorizationRequest checkForPreApproval(AuthorizationRequest authorizationRequest,
Authentication userAuthentication) {
return authorizationRequest;
}
public AuthorizationRequest updateAfterApproval(AuthorizationRequest authorizationRequest,
Authentication userAuthentication) {
return authorizationRequest;
}
public boolean isApproved(AuthorizationRequest authorizationRequest, Authentication userAuthentication) {
return true;
}
});
AuthorizationRequest authorizationRequest = getAuthorizationRequest("foo", "http://anywhere.com", "mystate",
"myscope", Collections.singleton("token"));
ModelAndView result = endpoint.authorize(model, authorizationRequest.getRequestParameters(), sessionStatus,
principal);
String url = ((RedirectView) result.getView()).getUrl();
assertTrue("Wrong view: " + result, url.startsWith("http://anywhere.com"));
assertTrue("Wrong state: " + result, url.contains("&state=mystate"));
assertTrue("Wrong token: " + result, url.contains("access_token="));
assertTrue("Wrong token: " + result, url.contains("foo=bar"));
}
@Test
public void testImplicitAppendsScope() throws Exception {
endpoint.setTokenGranter(new TokenGranter() {
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken("FOO");
token.setScope(Collections.singleton("read"));
return token;
}
});
endpoint.setUserApprovalHandler(new DefaultUserApprovalHandler() {
public AuthorizationRequest checkForPreApproval(AuthorizationRequest authorizationRequest,
Authentication userAuthentication) {
return authorizationRequest;
}
public AuthorizationRequest updateAfterApproval(AuthorizationRequest authorizationRequest,
Authentication userAuthentication) {
return authorizationRequest;
}
public boolean isApproved(AuthorizationRequest authorizationRequest, Authentication userAuthentication) {
return true;
}
});
AuthorizationRequest authorizationRequest = getAuthorizationRequest("foo", "http://anywhere.com", "mystate",
"myscope", Collections.singleton("token"));
ModelAndView result = endpoint.authorize(model, authorizationRequest.getRequestParameters(), sessionStatus,
principal);
String url = ((RedirectView) result.getView()).getUrl();
assertTrue("Wrong scope: " + result, url.contains("&scope=read"));
}
@Test
public void testImplicitWithQueryParam() throws Exception {
endpoint.setTokenGranter(new TokenGranter() {
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken("FOO");
return token;
}
});
endpoint.setUserApprovalHandler(new DefaultUserApprovalHandler() {
public boolean isApproved(AuthorizationRequest authorizationRequest, Authentication userAuthentication) {
return true;
}
});
AuthorizationRequest authorizationRequest = getAuthorizationRequest("foo", "http://anywhere.com?foo=bar",
"mystate", "myscope", Collections.singleton("token"));
ModelAndView result = endpoint.authorize(model, authorizationRequest.getRequestParameters(), sessionStatus,
principal);
String url = ((RedirectView) result.getView()).getUrl();
assertTrue("Wrong url: " + result, url.contains("foo=bar"));
}
@Test
public void testImplicitAppendsScopeWhenDefaulting() throws Exception {
endpoint.setTokenGranter(new TokenGranter() {
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken("FOO");
token.setScope(new LinkedHashSet<String>(Arrays.asList("read", "write")));
return token;
}
});
endpoint.setUserApprovalHandler(new DefaultUserApprovalHandler() {
public boolean isApproved(AuthorizationRequest authorizationRequest, Authentication userAuthentication) {
return true;
}
public AuthorizationRequest checkForPreApproval(AuthorizationRequest authorizationRequest,
Authentication userAuthentication) {
return authorizationRequest;
}
public AuthorizationRequest updateAfterApproval(AuthorizationRequest authorizationRequest,
Authentication userAuthentication) {
return authorizationRequest;
}
});
client.setScope(Collections.singleton("read"));
AuthorizationRequest authorizationRequest = getAuthorizationRequest("foo", "http://anywhere.com", "mystate",
null, Collections.singleton("token"));
ModelAndView result = endpoint.authorize(model, authorizationRequest.getRequestParameters(), sessionStatus,
principal);
String url = ((RedirectView) result.getView()).getUrl();
assertTrue("Wrong scope: " + result, url.contains("&scope=read%20write"));
}
@Test(expected = InvalidScopeException.class)
public void testImplicitPreApprovedButInvalid() throws Exception {
endpoint.setTokenGranter(new TokenGranter() {
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
throw new IllegalStateException("Shouldn't be called");
}
});
endpoint.setUserApprovalHandler(new DefaultUserApprovalHandler() {
public boolean isApproved(AuthorizationRequest authorizationRequest, Authentication userAuthentication) {
return true;
}
public AuthorizationRequest checkForPreApproval(AuthorizationRequest authorizationRequest,
Authentication userAuthentication) {
return authorizationRequest;
}
public AuthorizationRequest updateAfterApproval(AuthorizationRequest authorizationRequest,
Authentication userAuthentication) {
return authorizationRequest;
}
});
client.setScope(Collections.singleton("smallscope"));
AuthorizationRequest authorizationRequest = getAuthorizationRequest("foo", "http://anywhere.com", "mystate",
"bigscope", Collections.singleton("token"));
ModelAndView result = endpoint.authorize(model, authorizationRequest.getRequestParameters(), sessionStatus,
principal);
String url = ((RedirectView) result.getView()).getUrl();
assertTrue("Wrong view: " + result, url.startsWith("http://anywhere.com"));
}
@Test
public void testImplicitUnapproved() throws Exception {
endpoint.setTokenGranter(new TokenGranter() {
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
return null;
}
});
AuthorizationRequest authorizationRequest = getAuthorizationRequest("foo", "http://anywhere.com", "mystate",
"myscope", Collections.singleton("token"));
ModelAndView result = endpoint.authorize(model, authorizationRequest.getRequestParameters(), sessionStatus,
principal);
assertEquals("forward:/oauth/confirm_access", result.getViewName());
}
@Test
public void testImplicitError() throws Exception {
endpoint.setUserApprovalHandler(new DefaultUserApprovalHandler() {
public AuthorizationRequest checkForPreApproval(AuthorizationRequest authorizationRequest,
Authentication userAuthentication) {
return authorizationRequest;
}
public AuthorizationRequest updateAfterApproval(AuthorizationRequest authorizationRequest,
Authentication userAuthentication) {
return authorizationRequest;
}
public boolean isApproved(AuthorizationRequest authorizationRequest, Authentication userAuthentication) {
return true;
}
});
endpoint.setTokenGranter(new TokenGranter() {
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
return null;
}
});
AuthorizationRequest authorizationRequest = getAuthorizationRequest("foo", "http://anywhere.com", "mystate",
"myscope", Collections.singleton("token"));
ModelAndView result = endpoint.authorize(model, authorizationRequest.getRequestParameters(), sessionStatus,
principal);
String url = ((RedirectView) result.getView()).getUrl();
assertTrue("Wrong view: " + result, url.startsWith("http://anywhere.com"));
assertTrue("No error: " + result, url.contains("#error="));
assertTrue("Wrong state: " + result, url.contains("&state=mystate"));
}
@Test
public void testApproveOrDeny() throws Exception {
AuthorizationRequest request = getAuthorizationRequest("foo", "http://anywhere.com", null, null,
Collections.singleton("code"));
request.setApproved(true);
Map<String, String> approvalParameters = new HashMap<String, String>();
approvalParameters.put("user_oauth_approval", "true");
model.put("authorizationRequest", request);
View result = endpoint.approveOrDeny(approvalParameters, model, sessionStatus, principal);
assertTrue("Wrong view: " + result, ((RedirectView) result).getUrl().startsWith("http://anywhere.com"));
}
@Test
public void testApprovalDenied() throws Exception {
model.put("authorizationRequest",
getAuthorizationRequest("foo", "http://anywhere.com", null, null, Collections.singleton("code")));
Map<String, String> approvalParameters = new HashMap<String, String>();
approvalParameters.put("user_oauth_approval", "false");
View result = endpoint.approveOrDeny(approvalParameters, model, sessionStatus, principal);
String url = ((RedirectView) result).getUrl();
assertTrue("Wrong view: " + result, url.startsWith("http://anywhere.com"));
assertTrue("Wrong view: " + result, url.contains("error=access_denied"));
}
@Test
public void testDirectApproval() throws Exception {
ModelAndView result = endpoint.authorize(model,
getAuthorizationRequest("foo", "http://anywhere.com", null, "read", Collections.singleton("code"))
.getRequestParameters(), sessionStatus, principal);
// Should go to approval page (SECOAUTH-191)
assertFalse(result.getView() instanceof RedirectView);
}
@Test
public void testRedirectUriOptionalForAuthorization() throws Exception {
ModelAndView result = endpoint.authorize(model,
getAuthorizationRequest("foo", null, null, "read", Collections.singleton("code"))
.getRequestParameters(), sessionStatus, principal);
// RedirectUri parameter should be null (SECOAUTH-333), however the resolvedRedirectUri not
AuthorizationRequest authorizationRequest = (AuthorizationRequest) result.getModelMap().get(
"authorizationRequest");
assertNull(authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI));
assertEquals("http://anywhere.com", authorizationRequest.getRedirectUri());
}
/**
* Ensure that if the approval endpoint is called without a resolved redirect URI, the request fails.
* @throws Exception
*/
@Test(expected = InvalidRequestException.class)
public void testApproveOrDenyWithOAuth2RequestWithoutRedirectUri() throws Exception {
AuthorizationRequest request = getAuthorizationRequest("foo", null, null, null, Collections.singleton("code"));
request.setApproved(true);
Map<String, String> approvalParameters = new HashMap<String, String>();
approvalParameters.put("user_oauth_approval", "true");
model.put("authorizationRequest", request);
endpoint.approveOrDeny(approvalParameters, model, sessionStatus, principal);
}
private class StubAuthorizationCodeServices implements AuthorizationCodeServices {
private OAuth2Authentication authentication;
public String createAuthorizationCode(OAuth2Authentication authentication) {
this.authentication = authentication;
return "thecode";
}
public OAuth2Authentication consumeAuthorizationCode(String code) throws InvalidGrantException {
return authentication;
}
}
}