2

I have Spring Boot application and I attempted to integrate Apache shiro with it. As a first iteration, I am authenticating and authorizing the JWT way, no session whatsoever.

The way I have architected it, every REST request has to contain a JWT header that needs to be validated. I am doing it in a shiro filter. Post validation, filter sets a context, that any REST controller method will be able to fetch and act upon it.

I want the opinion of community to make sure my configuration is correct. Moreover, there are certain problems (at least IMO), I am facing with it. So if someone can throw some light on a correct way of handing it, would be greatly appreciated.

Following are some code snippets demonstrating my configuration and realm design.

Snippet 1 : ShiroConfiguration

private AuthenticationService authenticationService;
/**
 * FilterRegistrationBean
 * @return
 */
@Bean
public FilterRegistrationBean filterRegistrationBean() {
    FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
    filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
    filterRegistration.setEnabled(true);
    filterRegistration.setDispatcherTypes(DispatcherType.REQUEST);
    filterRegistration.setOrder(1);
    return filterRegistration;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager() {
    DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();
    dwsm.setRealm(authenticationService());
    final DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    // disable session cookie
    sessionManager.setSessionIdCookieEnabled(false);
    dwsm.setSessionManager(sessionManager);
    return dwsm;
}

/**
 * @see org.apache.shiro.spring.web.ShiroFilterFactoryBean
 * @return
 */
@Bean(name="shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager, JWTTimeoutProperties jwtTimeoutProperties, TokenUtil tokenUtil) {
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    bean.setSecurityManager(securityManager);

    //TODO: Create a controller to replicate unauthenticated request handler
    bean.setUnauthorizedUrl("/unauthor");

    Map<String, Filter> filters = new HashMap<>();
    filters.put("perms", new AuthenticationTokenFilter(jwtTimeoutProperties, tokenUtil));
    filters.put("anon", new AnonymousFilter());
    bean.setFilters(filters);

    LinkedHashMap<String, String> chains = new LinkedHashMap<>();
    chains.put("/", "anon");
    chains.put("/favicon.ico", "anon");
    chains.put("/index.html", "anon");
    chains.put("/**/swagger-resources", "anon");
    chains.put("/api/**", "perms");

    bean.setFilterChainDefinitionMap(chains);
    return bean;
}
@Bean
@DependsOn(value="lifecycleBeanPostProcessor")
public AuthenticationService authenticationService() {
    if (authenticationService==null){
        authenticationService = new AuthenticationService();
    }

    return  authenticationService;
}


@Bean
@DependsOn(value="lifecycleBeanPostProcessor")
public Authorizer authorizer() {
    return authenticationService();
}


@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
    proxyCreator.setProxyTargetClass(true);
    return proxyCreator;
}

Snippet 2 : AuthenticationFilter

public class AuthenticationTokenFilter extends PermissionsAuthorizationFilter {
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    String authorizationHeader = httpRequest.getHeader(TOKEN_HEADER);
    String authToken;

    String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
    httpRequest.setAttribute(alreadyFilteredAttributeName, true);

    AuthenticationService.ensureUserIsLoggedOut(); // To not end up getting following error.

    if (authorizationHeader != null && !authorizationHeader.isEmpty()) {

        if (authorizationHeader.startsWith(BEARER_TOKEN_START_WITH)) {
            authToken = authorizationHeader.substring(BEARER_TOKEN_START_INDEX);
        } else if (authorizationHeader.startsWith(BASIC_TOKEN_START_WITH)) {
            String caseId = UUID.randomUUID().toString();
            log.warn("{} Basic authentication is not supported but a Basic authorization header was passed in", caseId);
            return false;
        } else {
            // if its neither bearer nor basic, default it to bearer.
            authToken = authorizationHeader;
        }
        try {
            if(tokenUtil.validateTokenAgainstSignature(authToken, jwtTimeoutProperties.getSecret())) {
                Map<String, Object> outerClaimsFromToken = tokenUtil.getOuterClaimsFromToken(authToken, jwtTimeoutProperties.getSecret());

                JWTAuthenticationToken jwtAuthenticationToken = new JWTAuthenticationToken(outerClaimsFromToken.get(TokenUtil.CLAIM_KEY_USERID),
                                                (String) outerClaimsFromToken.get(TokenUtil.CLAIM_KEY_INNER_TOKEN));
                SecurityUtils.getSubject().login(jwtAuthenticationToken);

        } catch (JwtException | AuthenticationException ex) {
            log.info("JWT validation failed.", ex);
        }
    }
    return false;
}

Snippet 3 : TokenRestController

public Response getToken() {

    AuthenticationService.ensureUserIsLoggedOut(); // To not end up getting following error.
                                                        // org.apache.shiro.session.UnknownSessionException: There is no session with id

        // TODO: In case of logging in with the organization, create own token class implementing HostAuthenticationToken class.
        IAMLoginToken loginToken = new IAMLoginToken(authenticationRequestDTO.getUsername(), authenticationRequestDTO.getPassword());
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(loginToken);
        } catch (AuthenticationException e) {
            log.debug("Unable to login", e);
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null);
        }

        AuthenticatingUser user = (AuthenticatingUser) subject.getPrincipal();

            String authToken = authenticationService.generateToken(user);
            return ResponseEntity.status(HttpStatus.OK).body(new AuthenticationResponseDTO(authToken));
    });

Snippet 4 : AuthorizingRealm

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    if (token instanceof IAMLoginToken) {
        IAMLoginToken usernamePasswordToken = (IAMLoginToken) token;

        UserBO user = identityManagerRepository.getUserByUsername(usernamePasswordToken.getUsername(), true);

        if (user != null && user.getSecret() != null && !user.getSecret().isEmpty()) {
            if(passwordEncoder.matches(String.valueOf(usernamePasswordToken.getPassword()), user.getPassword())) {
                if (!isActive(user)) {
                    throw new AuthenticationException("User account inactive.");
                }
                return new SimpleAuthenticationInfo(toAuthenticatingUser(user).withSecret(user.getSecret()), usernamePasswordToken.getPassword(), getName());
            }
        }
    } else if (token instanceof JWTAuthenticationToken) {
        JWTAuthenticationToken jwtToken = (JWTAuthenticationToken) token;
        String userId = (String) jwtToken.getUserId();
        String secret = cache.getUserSecretById(userId, false);

        if (secret != null && !secret.isEmpty()) {
            Map<String, Object> tokenClaims = tokenUtil.getClaims(jwtToken.getToken(), secret);
            String orgId = (String) tokenClaims.get(TokenUtil.CLAIM_KEY_ORG);
            String email = (String) tokenClaims.get(TokenUtil.CLAIM_KEY_EMAIL);
            String firstName = (String) tokenClaims.get(TokenUtil.CLAIM_KEY_FIRSTNAME);
            String lastName = (String) tokenClaims.get(TokenUtil.CLAIM_KEY_LASTNAME);
            Set<String> permissions = (Set<String>) tokenClaims.get(TokenUtil.CLAIM_KEY_PERMISSIONS);

            return new SimpleAccount(new AuthenticatingUser(userId, orgId, email, firstName, lastName, permissions), jwtToken.getToken(), getName());
        }
    }

    throw new AuthenticationException("Invalid username/password combination!");
}

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    authorizationInfo.setStringPermissions(((AuthenticatingUser)principals.getPrimaryPrincipal()).getPermissions());
    return authorizationInfo;
}

Problems and issues

  • Same error as mentioned in here. Shiro complaining "There is no session with id xxx" with DefaultSecurityManager I basically want Shiro to stop using and/or validating sessions. Is there a way to achieve that ? I solved it by implementing the same fix as mentioned in the answer, thats what ensureUserIsLoggedOut() does.

  • As you can see in the configuration's ShiroFilterFactoryBean definition, I am setting some filter chain definitions. And there you can see I set every api call that starts with /api will have authentication filter in front. But the thing is I want to add some exceptions to it. Such as, /api/v0/login is one of them. Is there a way of achieving this ?

  • Overall, I am not sure if the configuration I have come up with is appropriate, as I found very limited documentation and similar open source project samples.

Any feedback is welcome.

2 Answers2

1

I resolved the first issue of unwanted session validation and management by stopping Shiro from using a Subject’s session to store that Subject’s state across requests/invocations/messages for all Subjects.

I just had to apply following configuration to my session manager in my shiro configuration. https://shiro.apache.org/session-management.html#disabling-subject-state-session-storage

0

You should probably separate Your token filter from the 'perms' filter. Take a look at the BasicAuth filter or 'authc' filter. That should help you get around the issues you are seeing. You are basically using an 'authz' filter (which i'm guessing is why you need those work arounds)

Brian Demers
  • 2,051
  • 1
  • 9
  • 12
  • Unfortunately I got the same effect. Both types of filters work and I am still getting same issues. But you're right in terms of using 'authc' filter. Its the right filter. – Chintan Barad Dec 04 '17 at 15:41