4

I'm using Tapestry-Security which uses Apache Shiro

I have a custom realm which handles authorization and authentication. Our authentication technically happens using a remote service, which returns a username and a set of roles. I just pass the username into my custom AuthenticationToken which allows me to query our local db and set the SimpleAuthenticationInfo.

I can't figure out how to populate the AuthorizationInfo doGetAuthorizationInfo method using the list of roles returned to me from our remote service. Below is the code I'm using to populate the realm.

Login.class

//Remote authentication service
RemoteLoginClient client = new RemoteLoginClient();
RemoteSubject authenticate = client.authenticate(username, password);

//tapestry security authentication
Subject currentUser = SecurityUtils.getSubject();
CustomAuthenticationToken token = new 
    CustomAuthenticationToken(authenticate.getUsername());
System.out.println("roles" + authenticate.getRoles());

currentUser.login(token);

AuthorizationInfo method inside customRealm public class CustomRealm extends AuthorizingRealm {

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    CustomAuthenticationToken upToken = (CustomAuthenticationToken ) token;
    String email = upToken.getUsername();

    ApplicationUser applicationUser = (ApplicationUser) session.createCriteria(ApplicationUser.class)
            .add(Restrictions.like("email", email + "%"))
            .uniqueResult();

    if (applicationUser == null) {
        throw new UnknownAccountException("User doesn't exist in EPRS database");
    }

    return buildAuthenticationInfo(applicationUser.getId());
}

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//Not sure how to populate the principle or
//read the principle to populate the SimpleAuthorizationInfo
    return new SimpleAuthorizationInfo(roleNames);
}
Code Junkie
  • 7,602
  • 26
  • 79
  • 141
  • Some weeks ago I wrote down how to deal with Shiro and Guice. However, could you check if the section "Implementing the Realms" will cover your question - http://readyareyou.blogspot.de/2012/03/guice-jersey-shiro.html ? – PepperBob May 01 '12 at 18:39
  • @PepperBob Thanks for the response, seems to me the example in the link you provided me is still using a database to look up the user roles. In my scenario the roles are being provided to me in a list from our web service at login. I would some how need to pass that list of roles into the SimpleAuthorizationInfo which is where I'm currently lost. Any additional thoughts? – Code Junkie May 01 '12 at 19:23
  • I think you should be able to stick everything into an implementation of the Account-Interface (http://shiro.apache.org/static/current/apidocs/org/apache/shiro/authc/Account.html) as it bundles Authentication and Authorization, eg. SimpleAccount or something custom made. – PepperBob May 01 '12 at 20:37

2 Answers2

7

Extending AuthorizingRealm is a good place to start if you need both authentication and authorization. Also, as PepperBob has already said, while you're at it, the Account interface and its SimpleAccount implementation support both authentication and authorization in a single interface, so you don't need much separate code for doGetAuthenticationInfo() and doGetAuthorizationInfo() and can just return the same object from both methods.

To get the authorization information, you need to do two things:

  • Get an available principal from the principal collection passed as a parameter (which, in most cases, just contains one principal anyway) via the getAvailablePrincipal() method (neatly predefined in AuthorizingRealm).
  • Load your roles and pass them to setRoles() on your account object.

...and you're done.

Edited to add:

This would be a very simple way to store the roles until you need them. Note that the actual authentication is done in the realm, which has a dependency on RemoteLoginClient.

public class MyRealm extends AuthorizingRealm {

    private RemoteLoginClient client = ...;

    private final Map<String, Set<String>> emailToRoles = new ConcurrentHashMap<>();

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
             AuthenticationToken token) {
        final UsernamePasswordToken userPass = (UsernamePasswordToken) token;

        final RemoteSubject authenticate = this.client.authenticate(
            userPass.getUserName(), userPass.getPassword());
        if (authenticate != null) { //assuming this means success
            this.emailToRoles.put(userPass.getUserName(), authenticate.getRoles());
            return new SimpleAuthenticationInfo(...);
        } else {
            return null;
        }
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(
            PrincipalCollection principals) {
         final String username = (String) principals.getPrimaryPrincipal();
         final Set<String> roles = this.emailToRoles.get(username);
         return new SimpleAuthorizationInfo(roles);
    }

}
Henning
  • 16,063
  • 3
  • 51
  • 65
  • Hi Henning, I'm not sure your suggestion answers my question. I posted my doGetAuthenticationInfo method above. In a post on the shiro mailing list, Les Hazelwood suggest you could preemptively construct and cache an AuthorizationInfo object during authentication so there is only one perceived 'hit' during login. You would do this by calling the getAuthorizationInfo(PrincipalCollection principals) method from within your doGetAuthenticationInfo method. I'm not sure how, you can check it out here http://shiro-user.582556.n2.nabble.com/Shiro-and-LDAP-authorization-td7096956.html#a7520967 Thanks – Code Junkie May 02 '12 at 17:49
  • In that case I don't think I fully understood the question. So you need to do authentication and authorization in one step, because your backend mandates that? Yes, you would have to cache the roles belonging to the user, that's right. What exactly is the problem you're having? Is it that you can't figure out how to store them so you can return them in `doGetAuthorizationInfo()`? If yes, why not just put them in a Map when you hit the backend? – Henning May 02 '12 at 21:50
  • I just posted a working solution. If you were to take a look at it, you'll see what I was trying to accomplish. Feel free to offer suggestions. I'm no Shiro expert by any means, so I'm sure there is room for improvement. Thanks Henning. – Code Junkie May 03 '12 at 13:12
  • 1
    @George: Okay, I see what you were after: You wanted to piggyback your roles on a custom authentication token so you wouldn't need to load them in `doGetAuthorizationInfo`. Personally I'd strongly prefer an extra round trip to the backend to fetch the roles (or a map to cache them until you need them) to adding them to the token, because the token is meant to represent the user's principal and credentials, and nothing else. – Henning May 03 '12 at 14:28
  • I agree the principal shouldn't be used for storing roles, however going to the backend for the roles wasn't an option due to the fact they are only being sent to me once during login from the RemoteLoginClient. The RemoteLoginClient is a web service which communicates with our crowd service. How would you map a cache to them? Thanks Henning. – Code Junkie May 03 '12 at 14:36
  • @George: I have added some code to my answer that demonstrates this. – Henning May 04 '12 at 08:11
3

I answered my own question and wanted to post this in case someone else needed help or for possible improvement on my solution.

Login.class method

Object onSubmit() {
    try {
        //Remote Authentication
        RemoteLoginClient client = new RemoteLoginClient ();
        RemoteSubject authenticate = client.authenticate(formatUsername(username), password);

        //tapestry security authentication
        Subject currentUser = SecurityUtils.getSubject();
        CustomAuthenticationToken token = new CustomAuthenticationToken(authenticate.getUsername(), authenticate.getRoles());

        currentUser.login(token);
    } //catch errors
}

//Custom token used to hold username and roles which are set from remote authentication service.

public class CustomAuthenticationToken implements AuthenticationToken {

private String username;
private Set<String> roles;

public CustomAuthenticationToken(String username, Set<String> roles) {
    this.username = username;
    this.roles = roles;
}

getters/setters

//Custom Realm used to handle local authentication and authorization.

public class CustomRealm extends AuthorizingRealm {

//Hibernate Session
private final Session session;
public static final String EMPTY_PASSWORD = "";

public CustomRealm(Session session) {
    this.session = session;
    setCredentialsMatcher(new AllowAllCredentialsMatcher());
    setAuthenticationTokenClass(CustomAuthenticationToken.class);
}

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    CustomAuthenticationToken customToken = (CustomAuthenticationToken) token;
    String email = customToken.getUsername();

    User user = (User) session.createCriteria(User.class)
            .add(Restrictions.like("email", email+ "%"))
            .uniqueResult();

    if (user == null) {
        throw new UnknownAccountException("User doesn't exist in local database");
    }

    return new SimpleAuthenticationInfo(new CustomPrincipal(user, customToken.getRoles()), EMPTY_PASSWORD, getName());
}

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    return new SimpleAuthorizationInfo(((CustomPrincipal) principals.getPrimaryPrincipal()).getRoles());
}

}

//Custom principal used to hold user object and roles public class CustomPrincipal {

private User user;
private Set<String> roles;

public CustomPrincipal() {
}

public CustomPrincipal(User user, Set<String> roles) {
    this.user = user;
    this.roles = roles;
}

getters/setters
Code Junkie
  • 7,602
  • 26
  • 79
  • 141