I am implementing an online platform using Java Restful Jersey with Apache Shiro for Authentication Authorization. My security implementation was based on article JSON Web Token with Apache Shiro. Following is my shiro.ini and implemented Classes.
shiro.ini
[main]
jwtg = gr.histopath.platform.lib.JWTGuard
jwtv = gr.histopath.platform.lib.JWTVerifyingFilter
ds = com.mysql.cj.jdbc.MysqlDataSource
ds.serverName = 127.0.0.1
ds.port = 3306
ds.user = histopathUser
ds.password = H1s+0p@+h.U$er
ds.databaseName = histopath
jdbcRealm = gr.histopath.platform.lib.MyRealm
jdbcRealm.dataSource = $ds
credentialsMatcher = org.apache.shiro.authc.credential.Sha512CredentialsMatcher
credentialsMatcher.hashIterations = 50000
credentialsMatcher.hashSalted = true
credentialsMatcher.storedCredentialsHexEncoded = false
jdbcRealm.credentialsMatcher = $credentialsMatcher
jdbcRealm.permissionsLookupEnabled = false
shiro.loginUrl = /authentication/login
#cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager
securityManager.cacheManager = $cacheManager
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
securityManager.sessionManager = $sessionManager
securityManager.sessionManager.globalSessionTimeout = 172800000
# ssl.enabled = false
securityManager.realms = $jdbcRealm
[users]
[roles]
[urls]
/authentication/login = authc
# /authentication/logout = logout
/search/* = noSessionCreation, jwtv
/statistics/* = noSessionCreation, jwtv
/clinics/* = noSessionCreation, jwtv
/patients/* = noSessionCreation, jwtv
/incidents/* = noSessionCreation, jwtv
/doctors/* = noSessionCreation, jwtv
/users/new = noSessionCreation, anon
/users/details/* = noSessionCreation, anon
/users/* = noSessionCreation, jwtv
/* = anon
MyRealm.java
package gr.histopath.platform.lib;
import gr.histopath.platform.model.DAO.UserDAO;
import gr.histopath.platform.model.TransferObjects.User;
import org.apache.shiro.authc.*;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.util.ByteSource;
public class MyRealm extends JdbcRealm {
private UserDAO userDAO;
private User user;
private String password;
private ByteSource salt;
public MyRealm() {
this.userDAO = new UserDAO();
setSaltStyle(SaltStyle.COLUMN);
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// identify account to log to
UsernamePasswordToken userPassToken = (UsernamePasswordToken) token;
String username = userPassToken.getUsername();
System.out.println("GMOTO: " + userPassToken.getUsername());
if (username.equals(null)) {
System.out.println("Username is null.");
return null;
}
// read password hash and salt from db
// System.out.println("Username: " + username);
if(!userDAO.isOpen()){
userDAO = new UserDAO();
}
this.user = userDAO.getByUsername(username);
this.userDAO.closeEntityManager();
System.out.println("user's email: " + this.user.getUsername());
if (this.user == null) {
System.out.println("No account found for user [" + username + "]");
return null;
}
this.password = this.user.getPassword();
this.salt = ByteSource.Util.bytes(Base64.decode(this.user.getSalt()));
SaltedAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, salt, getName());
return info;
}
}
MY JWT Verifying Filter:
package gr.histopath.platform.lib;
import gr.histopath.platform.model.TransferObjects.User;
import io.jsonwebtoken.*;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.DatatypeConverter;
public class JWTVerifyingFilter extends AccessControlFilter {
private static final Logger logger = LoggerFactory.getLogger(JWTVerifyingFilter.class);
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {
logger.debug("Verifying Filter Execution");
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
String jwt = httpRequest.getHeader("Authorization");
logger.debug("JWT Found");
if (jwt == null || !jwt.startsWith("Bearer ")) {
// System.out.println("DEn Brika Tipota: ");
logger.debug("No Token Found...");
// servletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
jwt = jwt.substring(jwt.indexOf(" "));
Subject subject = SecurityUtils.getSubject();
// System.out.println("Token Found");
// System.out.println("JWT: " + jwt);
// System.out.println("Authenticated? " + subject.isAuthenticated());
// System.out.println(" session " + subject.getSession().getId());
// System.out.println(" salt " + ((User) subject.getPrincipal()).getSalt());
// System.out.println(" who-is " + ((User) subject.getPrincipal()).getUsername());
User user = null;
if (subject.isAuthenticated()) {
user = (User) subject.getPrincipal();
String username = null;
try {
Jws<Claims> claimsJws = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(user.getSalt()))
.parseClaimsJws(jwt);
// System.out.println("Claims: " + claimsJws);
logger.debug("Expiration: " + claimsJws.getBody().getExpiration());
username = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(user.getSalt()))
.parseClaimsJws(jwt).getBody().getSubject();
} catch (ExpiredJwtException expiredException) {
logger.debug("Token Is Expired....");
logger.debug(expiredException.getMessage(), expiredException);
// System.out.println("Token IS Expired.....");
// expiredException.printStackTrace();
logger.debug("Logging out the user...");
// System.out.println("Logging out the user...");
SecurityUtils.getSubject().logout();
// System.out.println("mmmnnnnn: " + SecurityUtils.getSubject().isAuthenticated());
return false;
// throw expiredException;
} catch (SignatureException signatureException) {
logger.debug(signatureException.getMessage(), signatureException);
// signatureException.printStackTrace();
return false;
} catch (Exception e) {
logger.debug(e.getMessage(), e);
// e.printStackTrace();
return false;
}
// System.out.println("Subject: " + user.getUsername());
return username.equals(user.getUsername());
}
// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return false;
}
}
And JWT Guard
package gr.histopath.platform.lib;
import org.apache.shiro.web.filter.authc.AuthenticationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
public class JWTGuard extends AuthenticationFilter {
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
// System.out.println("JWT GUARD FIRED!!!!!");
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
}
Everything was working almost fine, except that randomly/occasionally, despite the user was logged in, a session timeout occurred and the system logged out the user despite the fact, the token has 7 day expiration period.
So i decided to try and make the system stateless without any sessions. To do so i used the command:
securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false
according to Disabling Subject State Session Storage
However, now i can't login at all. I get
java.lang.NullPointerException at gr.histopath.platform.lib.MyRealm.doGetAuthenticationInfo(MyRealm.java:31)
i.e. String username = userPassToken.getUsername(); //This is null
Now my shiri.ini looks as following:
Changed shiro.ini
[main]
jwtg = gr.histopath.platform.lib.JWTGuard
jwtv = gr.histopath.platform.lib.JWTVerifyingFilter
ds = com.mysql.cj.jdbc.MysqlDataSource
ds.serverName = 127.0.0.1
ds.port = 3306
ds.user = histopathUser
ds.password = H1s+0p@+h.U$er
ds.databaseName = histopath
jdbcRealm = gr.histopath.platform.lib.MyRealm
jdbcRealm.dataSource = $ds
credentialsMatcher = org.apache.shiro.authc.credential.Sha512CredentialsMatcher
credentialsMatcher.hashIterations = 50000
credentialsMatcher.hashSalted = true
credentialsMatcher.storedCredentialsHexEncoded = false
jdbcRealm.credentialsMatcher = $credentialsMatcher
jdbcRealm.permissionsLookupEnabled = false
shiro.loginUrl = /authentication/login
#cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager
securityManager.cacheManager = $cacheManager
#sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
#securityManager.sessionManager = $sessionManager
#securityManager.sessionManager.globalSessionTimeout = 172800000
securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false
# ssl.enabled = false
securityManager.realms = $jdbcRealm
[users]
[roles]
[urls]
/authentication/login = authc
# /authentication/logout = logout
/search/* = noSessionCreation, jwtv
/statistics/* = noSessionCreation, jwtv
/clinics/* = noSessionCreation, jwtv
/patients/* = noSessionCreation, jwtv
/incidents/* = noSessionCreation, jwtv
/doctors/* = noSessionCreation, jwtv
/users/new = noSessionCreation, anon
/users/details/* = noSessionCreation, anon
/users/* = noSessionCreation, jwtv
/* = anon
I haven't found any complete example of session less shiro. Any suggestions for my code to make it work?? I must be missing something but i don't know what.
- Why after disabling session MyRealm cannot read username from UsernamePasswordToken??
- Why in my first implementation a Session Timeout occasionally occurred. Any ideas on this??