Skip to content

Commit

Permalink
[cfid-348] Refactor app sample to use UAA scopes
Browse files Browse the repository at this point in the history
This change doesn't affect the server (provider) features of the UAA
but it does alter the way that the sample client works, and provides
additional library features to support that.  The basic idea is
that a client app can interrogate the token scopes and use them to
provide role-based (or other) authorization locally. The sample does
this with the /check_token endpoint (hence the app has to be a
uaa.resource), but it could in principle work with local decoding too.

Also inspired by problems encountered with a abug that surfaced in the
dashboard:

[#39194235] [cfid-300] User authenticated by Login Server only has restricted authorities (uaa.user)

Change-Id: I01e760fbe7b9689263af61a2165f8b0432342ac1
  • Loading branch information
dsyer committed Nov 28, 2012
1 parent b768603 commit 8bfcd39
Show file tree
Hide file tree
Showing 28 changed files with 358 additions and 148 deletions.
@@ -0,0 +1,170 @@
/*
* Cloud Foundry 2012.02.03 Beta
* Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*/

package org.cloudfoundry.identity.uaa.client;

import java.security.Principal;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.client.UserRedirectRequiredException;
import org.springframework.security.oauth2.client.http.AccessTokenRequiredException;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

/**
* An authentication filter for remote identity providers. Intended to be used with Spring OAuth (1 or 2), since it is
* aware of the redirect protocols employed by those frameworks. If used in the PRE_AUTH_FILTER position of a regular
* Spring Security filter chain the user will be redirected to the remote provider to approve the access and return with
* a valid access token. There are 2 main strategies to provide:
*
* <ul>
* <li> {@link #setPreAuthenticatedPrincipalSource(PreAuthenticatedPrincipalSource) PreAuthenticatedPrincipalSource}
* (mandatory) provides a {@link Principal} that can be authenticated by the authentication manager. An example would be
* to contact the user info endpoint in a remote social provider and populate an {@link Authentication} token with the
* user's profile data. The principal is wrapped by</li>
* <li>{@link #setAuthenticationManager(AuthenticationManager) Authentication manager} is optional and defaults to a
* value that tries very hard to authenticate everything it sees, on the assumption that it was obtained from a trusted
* ID provider.</li>
* </ul>
*
* To ensure that the default authentication manager successfully authenticates the user, the principal source should
* create a principal that itself is an {@link Authentication} and is already authenticated. If you are not using the
* default authentication manager then you are free to authenticate any way you like (hence there is collaboration
* between the principal source and authentication manager, and the principal source can create an object of any type
* that is understood by the authentication manager).
*
* @author Dave Syer
*
*/
public class ClientAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {

private PreAuthenticatedPrincipalSource<?> principalSource;

private boolean oauthAvailable = false;

private boolean oauth2Available = false;

/**
* @param socialClientUserDetailsSource the socialClientUserDetailsSource to set
*/
public void setPreAuthenticatedPrincipalSource(PreAuthenticatedPrincipalSource<?> principalSource) {
this.principalSource = principalSource;
}

@Override
public void afterPropertiesSet() {
Assert.state(principalSource != null, "User info source must be provided");
super.afterPropertiesSet();
try {
oauth2Available = ClassUtils.isPresent(AccessTokenRequiredException.class.getName(),
ClassUtils.getDefaultClassLoader());
}
catch (NoClassDefFoundError e) {
// ignore
}
try {
oauthAvailable = ClassUtils.isPresent(
org.springframework.security.oauth.consumer.AccessTokenRequiredException.class.getName(),
ClassUtils.getDefaultClassLoader());
}
catch (NoClassDefFoundError e) {
// ignore
}
}

public ClientAuthenticationFilter(String defaultFilterProcessesUrl) {
setAuthenticationManager(new DefaultFriendlyAuthenticationManager());
}

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) {
// Need to force a redirect via the OAuth client filter, so rethrow here if OAuth related
if (oauth2Available && failed instanceof SocialRedirectException) {
throw ((SocialRedirectException) failed).getUserRedirectException();
}
if (oauthAvailable
&& failed instanceof org.springframework.security.oauth.consumer.AccessTokenRequiredException) {
throw failed;
}
else {
// If the exception is not a Spring Security exception this will result in a default error page
super.unsuccessfulAuthentication(request, response, failed);
}
}

@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
try {
Object result = principalSource.getPrincipal();
return result;
}
catch (UserRedirectRequiredException e) {
throw new SocialRedirectException(e);
}
}

@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return "N/A";
}

private static class DefaultFriendlyAuthenticationManager implements AuthenticationManager {

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

boolean authenticated = authentication.isAuthenticated();

// If not already authenticated (the default) from the parent class
if (authentication instanceof PreAuthenticatedAuthenticationToken && !authenticated) {

PreAuthenticatedAuthenticationToken preAuth = (PreAuthenticatedAuthenticationToken) authentication;
// Look inside the principal and see if that was marked as authenticated
if (preAuth.getPrincipal() instanceof Authentication) {
Authentication principal = (Authentication) preAuth.getPrincipal();
preAuth = new PreAuthenticatedAuthenticationToken(principal, preAuth.getCredentials(), principal.getAuthorities());
authenticated = principal.isAuthenticated();
}
preAuth.setAuthenticated(authenticated);

authentication = preAuth;

}

return authentication;

}

}

private static class SocialRedirectException extends AuthenticationException {

public SocialRedirectException(UserRedirectRequiredException e) {
super("Social user details extraction failed", e);
}

public UserRedirectRequiredException getUserRedirectException() {
return (UserRedirectRequiredException) getCause();
}

}

}
@@ -0,0 +1,50 @@
/*
* Cloud Foundry 2012.02.03 Beta
* Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*/

package org.cloudfoundry.identity.uaa.client;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.util.Assert;

/**
*
* @author Dave Syer
*
*/
public class OAuth2AccessTokenSource implements InitializingBean, PreAuthenticatedPrincipalSource<String> {

private OAuth2RestOperations restTemplate;

/**
* A rest template to be used to contact the remote user info endpoint. Normally an instance of
* {@link OAuth2RestTemplate}.
*
* @param restTemplate a rest template
*/
public void setRestTemplate(OAuth2RestOperations restTemplate) {
this.restTemplate = restTemplate;
}

@Override
public void afterPropertiesSet() {
Assert.state(restTemplate != null, "RestTemplate URL must be provided");
}

@Override
public String getPrincipal() {
return restTemplate.getAccessToken().getValue();
}

}
@@ -0,0 +1,24 @@
/*
* Cloud Foundry 2012.02.03 Beta
* Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*/

package org.cloudfoundry.identity.uaa.client;

/**
* @author Dave Syer
*
*/
public interface PreAuthenticatedPrincipalSource<T> {

T getPrincipal();

}
Expand Up @@ -10,20 +10,20 @@
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*/
package org.cloudfoundry.identity.uaa.social;
package org.cloudfoundry.identity.uaa.client;

import java.util.Collection;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

/**
* Customized {@code UserDetails} implementation.
*
* @author Luke Taylor
* @author Dave Syer
*/
public class SocialClientUserDetails extends User {
public class SocialClientUserDetails extends AbstractAuthenticationToken {

public static class Source {

Expand Down Expand Up @@ -69,6 +69,8 @@ else if (userInfoUrl.contains("linkedin.com")) {
}
}

private String username;

private String email;

private String name;
Expand All @@ -78,7 +80,9 @@ else if (userInfoUrl.contains("linkedin.com")) {
private String source;

public SocialClientUserDetails(String username, Collection<? extends GrantedAuthority> authorities) {
super(username, "unused", authorities);
super(authorities);
setAuthenticated(authorities!=null && !authorities.isEmpty());
this.username = username;
}

public String getEmail() {
Expand All @@ -98,12 +102,17 @@ public void setEmail(String email) {
}

public String getName() {
return name;
// This is used as the principal name (which could then be used to look up tokens etc)
return username;
}

public void setName(String name) {
public void setFullName(String name) {
this.name = name;
}

public String getFullName() {
return this.name;
}

public String getSource() {
return source;
Expand All @@ -112,4 +121,18 @@ public String getSource() {
public void setSource(String source) {
this.source = source;
}

public String getUsername() {
return username;
}

@Override
public Object getCredentials() {
return "N/A";
}

@Override
public Object getPrincipal() {
return this.username;
}
}
Expand Up @@ -11,14 +11,15 @@
* subcomponent's license, as noted in the LICENSE file.
*/

package org.cloudfoundry.identity.uaa.social;
package org.cloudfoundry.identity.uaa.client;

import java.util.List;
import java.util.Map;

import org.cloudfoundry.identity.uaa.social.SocialClientUserDetails.Source;
import org.cloudfoundry.identity.uaa.client.SocialClientUserDetails.Source;
import org.cloudfoundry.identity.uaa.user.UaaAuthority;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.util.Assert;
import org.springframework.web.client.RestOperations;
Expand All @@ -41,7 +42,7 @@
* @author Dave Syer
*
*/
public class SocialClientUserDetailsSource implements InitializingBean {
public class SocialClientUserDetailsSource implements InitializingBean, PreAuthenticatedPrincipalSource<Authentication> {

private RestOperations restTemplate;

Expand Down Expand Up @@ -73,7 +74,7 @@ public void afterPropertiesSet() {
Assert.state(userInfoUrl != null, "User info URL must be provided");
Assert.state(restTemplate != null, "RestTemplate URL must be provided");
}

/**
* Get as much generic information as possible about the current user from the remote endpoint. The aim is to
* collect as much of the properties of a {@link SocialClientUserDetails} as possible but not to fail if there is an
Expand All @@ -82,7 +83,8 @@ public void afterPropertiesSet() {
*
* @return some user details
*/
public SocialClientUserDetails getUserDetails() {
@Override
public Authentication getPrincipal() {
@SuppressWarnings("unchecked")
Map<String, String> map = restTemplate.getForObject(userInfoUrl, Map.class);
String userName = getUserName(map);
Expand All @@ -102,7 +104,7 @@ public SocialClientUserDetails getUserDetails() {
user.setExternalId(getUserId(map));
String fullName = getFullName(map);
if (fullName != null) {
user.setName(fullName);
user.setFullName(fullName);
}
if (email != null) {
user.setEmail(email);
Expand Down

0 comments on commit 8bfcd39

Please sign in to comment.