Search code examples
jakarta-eeshiroremember-me

Load remembered user data with Apache Shiro


I'm using Apache Shiro 1.4.0 with Java EE 7 on a Payara 4.1.1.x. My shiro.ini looks like:

[urls]
page1.xhtml = user
must_be_logged.xhtml = authc

The standard RememberMe functionality works perfectly on page1.xhtml

Regarding the login, I'm using a programmatic login from BalusC's article. As it's a CDI bean, I can inject some EJB and perform some actions:

@Named
@RequestScoped
public class Login{

    // username, password, rememberMe attribute definition with getter and setter

    @EJB
    private SomeService someServiceImpl;

    public void submit(){
        try {
            SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password, remember));

            Session session = SecurityUtils.getSubject().getSession(false);
            if(session != null){
                someServiceImpl.addUserInfoToSession(username, session);
            }
            // redirect to page
        }
        catch(AuthenticationException e) {
            // send error message
        }
    }
}

The addSomeUserInfoToSession basically retrieves a User entity from the database and add some information (first name, last name, language preferences, etc) to the session attributes via session.setAttribute("first_name", ...);.

I could not find how to perform such action for remembered users: When a subject is identified from a rememberMe cookie, how to perform a EJB-involved action?

  1. I could not find the filter where the rememberMe mecanism is triggered
  2. I tried using session listeners (implementing org.apache.shiro.session.SessionListener) but I could not find how to inject CDI bean or how to use EJB in such "POJO"
  3. If I use a custom RememberMeManager like extending org.apache.shiro.mgt.AbstractRememberMeManager, I could not find how to inject @EJB in it.

Solution

  • Self reply for my own record and if it may help others

    RememberMeManager

    Well, I'm an idiot, this was a wrong hint. Configuring RememberMeManager will only change "how the successfully logged principal will be remembered". As I'm using a web environment, the remember me functionality is cookie-based.

    However, it was not completely useless. By configuration RememberMeManager's cookie, I can choose the remember me cookie validity. By default, such cookie is valid for one year. It's then easy to set this duration at let's say 30 days:

    rememberMeCookie = org.apache.shiro.web.servlet.SimpleCookie
    rememberMeCookie.name = my_remember_me_name
    # 60*60*24*30
    rememberMeCookie.maxAge = 2592000
    
    rememberMeManager = org.apache.shiro.web.mgt.CookieRememberMeManager
    rememberMeManager.cookie = $rememberMeCookie
    rememberMeManager.cipherKey = **whatever you want but don't leave default value!**
    securityManager.rememberMeManager = $rememberMeManager
    

    Session Listener

    When sessions are instantiated, subject is already created so I had to search one step ahead

    SecurityManager

    By digging into TRACE logs, I reached the DefaultSecurityManager method: resolvePrincipals(SubjectContext). Okay this is where rememberMe cookie are caught. Principal information are fetched by the appropriate RememberMeManager (deciphering required) and put into a context. So far, the principal is fetched but I cannot proceed to any manipulation.

    resolvePrincipals(SubjectContext) is called by createSubject(SubjectContext) still within DefaultSecurityManager. This method ensures that a subject is always linked to the proper context and generate a subject if necessary. This leads to the SubjectFactory via the doCreateSubject(SubjectContext) method

    SubjectFactory

    So I tried to extend org.apache.shiro.mgt.DefaultSubjectFactory and assign it to the SecurityManager:

    mySubjectFactory = com.example.shiro.MySubjectFactory
    securityManager.subjectFactory = $mySubjectFactory
    

    First of all, I'm working in a web environment so the default implementation of security manager is not DefaultSecurityManager but org.apache.shiro.web.mgt.DefaultWebSecurityManager which requires that all subjects are compliant with the WebSubject interface. Consequently, I had to extend the org.apache.shiro.web.mgt.DefaultWebSubjectFactory instead.

    Okay, now I have the remembered subject: I just need to inject some @EJB and that's it! Well...no: Shiro is not the best friend of Dependency Injection. My quickest workaround is to use some CDI BeanManager to notify an ApplicationScoped CDI bean which will do the job. So my subject factory looks like:

    public class MySubjectFactory extends DefaultWebSecurityManager{
        private final BeanManager beanManager = CDI.current().getBeanManager();
    
        @Override
        public Subject createSubject(SubjectContext context){
            Subject subject = super.createSubject(context);
    
            // only do the job for remembered subjects. Need to
            // check only isRemembered() as DelegatingSubject's 
            // isRemembered() implementation makes it exclusive with
            // isAuthenticated()
            if(subject.isRemembered()){
                // getSession() with true flag to explicitly shows that
                // we have to create a session here. At this stage, session
                // is null so the event will have a null session as content
                Session session = subject.getSession(true);
    
                // Annotations are stripped for clarity 
                beanManager.fireEvent(session);
            }
        }
    }
    

    On the other hand, I have

    @ApplicationScoped
    public class MyShiroSessionManager{
    
        @EJB
        private MyFacade myEjb;
    
        public void onRememberedSubject(@Observes Session session){
            myEjb.updateUserInformation(session);
        }
    }
    

    This is not an optimized solution

    This does not sound an optimised solution but at least it works. If you have any hint regarding a better design, feel free to let me know.