Apache Shiro + CDI

Hello,

I have an application that uses apache shiro for authentication. I followed the approach mentioned over here:
http://mikepilone.blogspot.de/2013/07/vaadin-shiro-and-push.html

But now I want to switch from Spring to CDI, so I played a bit around on how to integrate shiro without the possibility to define the security manager etc. in a beans.xml.

I came across this example:
https://github.com/peterl1084/cdiexample
which is working nice as long as Vaadin-Push using Websockets is not used.

Right now everything works ok (WITH push) and as expected if I just simply programmatically initialize the security manager in my UI class:

Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

And additionally use a shiro.ini file to specify the realm etc. The CDIViewProvider handles the authorization correctly for annotated views.

So my question is if there are any possible disadvantages of this approach in an CDI-driven JEE7 application?

I think I answered the question myself… This will not work as closing and reopening a tab will not recognize the user is logged in. This seems to be due to the fact that the SecurityManager is initialized in the UI.

Any suggestions to fix this?

Just for those struggling with the same problem (using shiro in an CDI Environment), I found a solution.

I basically adapted the solution of Mike Pilone. First of all you need a class attaching the shiro subject to a Vaadin session, just like mentioned in the article of Mike Pilone (caused by the fact that the shiro SecurityUtils use threadlocal). I just changed it to correspond the static Shiro SecurityUtils:


import org.apache.shiro.SecurityUtils;
import org.apache.shiro.UnavailableSecurityManagerException;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;

import com.vaadin.server.VaadinSession;

public class VaadinSecurityUtils extends SecurityUtils {

    private static final String SUBJECT_ATTRIBUTE = VaadinSecurityUtils.class.getName() + ".subject";
    private static final String INI_RESOURCE_PATH = "classpath:shiro.ini";
    private static SecurityManager securityManager = managerFromIni();

    private static SecurityManager managerFromIni() {
        IniSecurityManagerFactory iniSecurityManagerFactory = new IniSecurityManagerFactory(INI_RESOURCE_PATH);
        DefaultSecurityManager manager = (DefaultSecurityManager) iniSecurityManagerFactory.getInstance();
        manager.setRememberMeManager(new VaadinCookieRememberMeManager());
        return manager;
    }

    public static Subject getSubject() {
        VaadinSession session = VaadinSession.getCurrent();
        if (session == null) {
            throw new IllegalStateException("Unable to locate VaadinSession to store Shiro Subject.");
        }

        Subject subject = (Subject) session.getAttribute(SUBJECT_ATTRIBUTE);
        if (subject == null) {
            subject = new Subject.Builder(getSecurityManager()).buildSubject();
            session.setAttribute(SUBJECT_ATTRIBUTE, subject);
        }

        return subject;
    }

    public static void setSecurityManager(SecurityManager securityManager) {
        VaadinSecurityUtils.securityManager = securityManager;
    }

    public static SecurityManager getSecurityManager()
            throws UnavailableSecurityManagerException {

        SecurityManager securityManager = VaadinSecurityUtils.securityManager;
        if (securityManager == null) {
            String msg = "No SecurityManager accessible to the calling code.";
            throw new UnavailableSecurityManagerException(msg);
        }

        return securityManager;
    }
}

Besides that I implemented a custom CookieRememberMeManager, to provide the standard shiro remember-me functionality (the shiro-one only works with a WebSecurityManager, and even there it did not work out for me).

Be aware that this remember-me mechanism is not save to stealing the cookie (see
http://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?lq=1
, Part II).


Furthermore this remember-me will of course only work in push modes that do not use websockets, as for websockets the cookies can’t be set (so either streaming or long polling).

[code]

import static org.apache.shiro.web.mgt.CookieRememberMeManager.DEFAULT_REMEMBER_ME_COOKIE_NAME;
import static org.apache.shiro.web.servlet.Cookie.DELETED_COOKIE_VALUE;
import static org.apache.shiro.web.servlet.Cookie.ONE_YEAR;
import static org.apache.shiro.web.servlet.Cookie.ROOT_PATH;

import javax.servlet.http.Cookie;

import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.AbstractRememberMeManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.server.VaadinService;

public class VaadinCookieRememberMeManager extends AbstractRememberMeManager {

private static transient final Logger log = LoggerFactory.getLogger(CookieRememberMeManager.class);
private Cookie cookie;

public VaadinCookieRememberMeManager() {
    Cookie cookie = new Cookie(DEFAULT_REMEMBER_ME_COOKIE_NAME, "");
    cookie.setMaxAge(ONE_YEAR);
    cookie.setPath(ROOT_PATH);
    cookie.setHttpOnly(true);
    this.cookie = cookie;
}

public Cookie getCookie() {
    return cookie;
}

protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {

    //base 64 encode it and store as a cookie:
    String base64 = Base64.encodeToString(serialized);

    Cookie cookie = copyTemplate();
    cookie.setValue(base64);
    VaadinService.getCurrentResponse().addCookie(cookie);
}

private Cookie copyTemplate() {
    Cookie template = getCookie();
    Cookie cookie = new Cookie(template.getName(), "");
    cookie.setMaxAge(template.getMaxAge());
    cookie.setPath(template.getPath());
    cookie.setHttpOnly(template.isHttpOnly());
    return cookie;
}

protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
    Cookie cookie = getCookieByName(getCookie().getName());
    String base64 = cookie != null ? cookie.getValue() : null;

    // Browsers do not always remove cookies immediately (SHIRO-183)
    // ignore cookies that are scheduled for removal
    if (DELETED_COOKIE_VALUE.equals(base64)) {
        return null;
    }

    if (base64 != null) {
        base64 = ensurePadding(base64);
        if (log.isTraceEnabled()) {
            log.trace("Acquired Base64 encoded identity [" + base64 + "]

");
}
byte decoded = Base64.decode(base64);
if (log.isTraceEnabled()) {
log.trace(“Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.”);
}
return decoded;
} else {
return null;
}
}

private String ensurePadding(String base64) {
    int length = base64.length();
    if (length % 4 != 0) {
        StringBuilder sb = new StringBuilder(base64);
        for (int i = 0; i < length % 4; ++i) {
            sb.append('=');
        }
        base64 = sb.toString();
    }
    return base64;
}

protected void forgetIdentity(Subject subject) {
    Cookie cookie = getCookieByName(getCookie().getName());
    if (cookie != null) {
        cookie.setPath(getCookie().getPath());
        cookie.setValue(DELETED_COOKIE_VALUE);
        cookie.setMaxAge(0);
        VaadinService.getCurrentResponse().addCookie(cookie);
    }
}

public void forgetIdentity(SubjectContext subjectContext) {
    forgetIdentity(subjectContext.getSubject());
}

private Cookie getCookieByName(String name) {
    Cookie[] cookies = VaadinService.getCurrentRequest().getCookies();
    for (Cookie cookie : cookies) {
        if (name.equals(cookie.getName())) {
            return cookie;
        }
    }

    return null;
}

}

[/code]And the last thing one has to do is to speciallize the JaasAccessControl class, to ensure this class will be injected for access control and use the shiro mechanisms.


import javax.enterprise.inject.Specializes;

import com.vaadin.cdi.access.JaasAccessControl;

@Specializes
public class ShiroAccessControl extends JaasAccessControl {

    @Override
    public boolean isUserSignedIn() {
        return VaadinSecurityUtils.getSubject().isAuthenticated();
    }

    @Override
    public boolean isUserInRole(String role) {
        return VaadinSecurityUtils.getSubject().hasRole(role);
    }

    @Override
    public String getPrincipalName() {
        Object principal = VaadinSecurityUtils.getSubject().getPrincipal();
        return principal != null ? principal.toString() : null;
    }
}

​By consequence one can now use the default JEE security annotations for the views and a standard shiro ini-file on the classpath to configure shiro.

Hello,

IMHO, auto-login with remember-me cookie should take place in your first request, i.e. the HTTP one. It should work whatever transport mechanism in use.

As a side note, Shiro seems to be “natively” supported by atmosphere through
ShiroInterceptor
, it creates a request attribute that contains your Shiro subject…

Hi Everybody,

I’m taking that subject out of ground because I’m having a related issue.

I used JDC approach to enable shiro into my Vaadin application, but I’m having issues with sessions.

After random time, when trying to check shiro subject permissions, it shows an “UnknownSessionException : there is no session with id”

I assume it’s due to something with the session manager but I have hard time finding what could happen…

I’ve only declared a custom realm in my shiro ini file, that validate users through our database, and the implemntation of JDC to make it work.

Thanks a lot, any help or hint would be great !!

Cheers