Last time I added username and password based authentication with using Spring Security. Should you have missed the that, I notice here that
JWT
tokens were issued upon a successful login and validated for subsequent requests. Creating long-lived JWTs isn’t practical, as they’re self contained and there’s no way to revoke them. If tokens are stolen all bets are off. For that reason, I wanted to add the classic remember-me style authentication with persistent tokens. Remember-me tokens are stored in cookies as JWTs as the first line of defense, however they are also persisted to the database and their lifecycle is being tracked.
This time I’d like to start with demonstrating how the running user management app works and later dive into the details.
Basically what happens in that users authenticate with a username / password pair and they might indicate their intention that they want the app to remember them (persistent session). Most of the time there’s an additional checkbox on the UI to make that happen. As the app hasn’t had a UI developed yet, we do everything with cURL.
curl -D- -c cookies.txt -b cookies.txt \
-XPOST http://localhost:5000/auth/login \
-d '{ "username":"test", "password": "test", "rememberMe": true }'
HTTP/1.1 200
...
Set-Cookie: remember-me=eyJhbGciOiJIUzUxMiJ9...;Max-Age=1209600;path=/;HttpOnly
X-Set-Authorization-Bearer: eyJhbGciOiJIUzUxMiJ9...
Upon successful authentication the
PersistentJwtTokenBasedRememberMeServices
creates a persistent Session,
saves it to the database and converts it into a JWT token. It takes care of storing this persistent session to a
cookie on the client’s side (Set-Cookie
) and it also sends the newly created transient token. The latter is
meant to be used through the lifetime of the single page front-end and sent with a non-standard HTTP header
(X-Set-Authorization-Bearer
).
When the rememberMe
flag is false
, just a stateless JWT token is created and the remember-me infrastructure is
completely bypassed.
While the app is open in the browser, it sends the transient JWT
token in the Authorization
header with every XHR
request. When the application gets reloaded however, the transient token gets lost. For the sake of simplicity
normal GET /users/{id}
is used here to demonstrate a normal request.
curl -D- -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...' \
-XGET http://localhost:5000/users/524201457797040
HTTP/1.1 200
...
{
"id" : 524201457797040,
"screenName" : "test",
"contactData" : {
"email" : "test@craftingjava.com",
"addresses" : [ ]
},
"timezone" : "AMERICA_LOS_ANGELES",
"locale" : "en_US"
}
This happens when the user has selected remember-me
authentication in the first scenario.
curl -D- -c cookies.txt -b cookies.txt \
-H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...' \
-XGET http://localhost:5000/users/524201457797040
HTTP/1.1 200
...
{
"id" : 524201457797040,
"screenName" : "test",
"contactData" : {
"email" : "test@craftingjava.com",
"addresses" : [ ]
},
"timezone" : "AMERICA_LOS_ANGELES",
"locale" : "en_US"
}
In this scenario both the transient JWT token and a valid remember-me
cookie are sent at the same time. As long as
the single page application is running, the transient token is used.
When the front-end gets loaded in the browser, it doesn’t know about the existence of any transient JWT tokens.
All it can do is testing the persisted remember-me
cookie by trying to execute a normal request.
curl -D- -c cookies.txt -b cookies.txt \
-XGET http://localhost:5000/users/524201457797040
HTTP/1.1 200
...
Set-Cookie: remember-me=eyJhbGciOiJIUzUxMiJ9...;Max-Age=1209600;path=/;HttpOnly
X-Set-Authorization-Bearer: eyJhbGciOiJIUzUxMiJ9...
{
"id" : 524201457797040,
"screenName" : "test",
"contactData" : {
"email" : "test@craftingjava.com",
"addresses" : [ ]
},
"timezone" : "AMERICA_LOS_ANGELES",
"locale" : "en_US"
}
If the persistent token (cookie) is still valid it gets updated in the database keeping on record the last time it was used and it also gets updated in the browser. Another important step is also performed, the user gets authenticated automatically again without having to give their username / password pair and a new transient token is created. From now on, the app uses the transient token as long as it’s running.
Altought logging out seems simple, there are a few details we need to be aware of. The front-end still send the stateless JWT token, as long as the user is authenticated, otherwise the logout button on UI wouldn’t even be offered and the back-end wouldn’t know how logs out.
curl -D- -c cookies.txt -b cookies.txt \
-H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...' \
-XPOST http://localhost:5000/auth/logout
HTTP/1.1 302
Set-Cookie: remember-me=;Max-Age=0;path=/
Location: http://localhost:5000/login?logout
After this request the remember-me
cookie get reset and also the persistent session in the database flagged as deleted.
As I mentioned in the summary, we’re going to use persistent tokens for added security in order to be able to revoke
them any time we wish. There are three steps we need to perform to enable proper remember-me
handling with Spring Security.
In the first post, I decided that the model will be developed with DDD, thus it couldn’t depend on any framework
specific class. Actually, it doesn’t even depend on any 3rd party framework or library. Most tutorials usually just
implement UserDetailsService
directly and there’s no extra layer between the business logic and the framework used
to build the application.
UserServices
was added to the project long ago in the second part, thus our task is quite simple, because all we
need now is a framework specific component which delegates the responsibility of a UserDetailsService
to the existing logic.
public class DelegatingUserService implements UserDetailsService {
private final UserService userService;
public DelegatingUserService(UserService userService) {
this.userService = userService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Long userId = Long.valueOf(username);
UsernameNotFoundException usernameNotFoundException = new UsernameNotFoundException(username);
return userService.findUser(userId)
.map(DelegatingUser::new)
.orElseThrow(() -> usernameNotFoundException);
}
}
Is just a simple wrapper around UserService
which eventually converts the returned
User
model object to a framework specific UserDetails
instance. Other than that, in this project we don’t use the user’s login name (email address or screen name) directly.
Instead their users’ ID is passed around everywhere.
Fortunately we have an equally easy job in adding a proper PersistentTokenRepository
implementation, as the domain
model already contains SessionService
and Session.
public class DelegatingPersistentTokenRepository implements PersistentTokenRepository {
private static final Logger LOGGER =
LoggerFactory.getLogger(DelegatingPersistentTokenRepository.class);
private final SessionService sessionService;
public DelegatingPersistentTokenRepository(SessionService sessionService) {
this.sessionService = sessionService;
}
@Override
public void createNewToken(PersistentRememberMeToken token) {
Long sessionId = Long.valueOf(token.getSeries());
Long userId = Long.valueOf(token.getUsername());
sessionService.createSession(sessionId, userId, token.getTokenValue());
}
@Override
public void updateToken(String series, String tokenValue, Date lastUsed) {
Long sessionId = Long.valueOf(series);
try {
sessionService.useSession(sessionId, tokenValue, toLocalDateTime(lastUsed));
} catch (NoSuchSessionException e) {
LOGGER.warn("Session {} doesn't exists.", sessionId);
}
}
@Override
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
Long sessionId = Long.valueOf(seriesId);
return sessionService
.findSession(sessionId)
.map(this::toPersistentRememberMeToken)
.orElse(null);
}
@Override
public void removeUserTokens(String username) {
Long userId = Long.valueOf(username);
sessionService.logoutUser(userId);
}
private PersistentRememberMeToken toPersistentRememberMeToken(Session session) {
String username = String.valueOf(session.getUserId());
String series = String.valueOf(session.getId());
LocalDateTime lastUsedAt =
Optional.ofNullable(session.getLastUsedAt()).orElseGet(session::getIssuedAt);
return new PersistentRememberMeToken(
username, series, session.getToken(), toDate(lastUsedAt));
}
}
The situation is roughly the same as with UserDetailsService
, the wrapper converts between PersistentRememberMeToken
and Session
. The only thing which was taken extra care of is the date field in PersistentRememberMeToken
.
In Session, I separated that two date fields (ie. issuedAt
and lastUsedAt
) and the latter gets its first value when
the user first logs in with the help of a remember-me
token. Hence there’s chance that it’s null and when it is,
the value of issuedAt
is used instead.
At this point we re-use
PersistentTokenBasedRememberMeServices
and customize for the task at hand, it depends on both UserDetailsService
and PersistentTokenRepository
and those were already taken care of.
public class PersistentJwtTokenBasedRememberMeServices extends
PersistentTokenBasedRememberMeServices {
private static final Logger LOGGER =
LoggerFactory.getLogger(PersistentJwtTokenBasedRememberMeServices.class);
public static final int DEFAULT_TOKEN_LENGTH = 16;
public PersistentJwtTokenBasedRememberMeServices(
String key, UserDetailsService userDetailsService,
PersistentTokenRepository tokenRepository) {
super(key, userDetailsService, tokenRepository);
}
@Override
protected String[] decodeCookie(String cookieValue) throws InvalidCookieException {
try {
Claims claims = Jwts.parser()
.setSigningKey(getKey())
.parseClaimsJws(cookieValue)
.getBody();
return new String[] { claims.getId(), claims.getSubject() };
} catch (JwtException e) {
LOGGER.warn(e.getMessage());
throw new InvalidCookieException(e.getMessage());
}
}
@Override
protected String encodeCookie(String[] cookieTokens) {
Claims claims = Jwts.claims()
.setId(cookieTokens[0])
.setSubject(cookieTokens[1])
.setExpiration(new Date(currentTimeMillis() + getTokenValiditySeconds() * 1000L))
.setIssuedAt(new Date());
return Jwts.builder()
.setClaims(claims)
.signWith(HS512, getKey())
.compact();
}
@Override
protected String generateSeriesData() {
long seriesId = IdentityGenerator.generate();
return String.valueOf(seriesId);
}
@Override
protected String generateTokenData() {
return RandomUtil.ints(DEFAULT_TOKEN_LENGTH)
.mapToObj(i -> String.format("%04x", i))
.collect(Collectors.joining());
}
@Override
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
return Optional.ofNullable((Boolean)request.getAttribute(REMEMBER_ME_ATTRIBUTE)).orElse(false);
}
}
This particular implementation uses JWT
tokens as the materialized form for storing remember-me
tokens in cookies.
Spring Security’s default form could have been just fine as well, but JWT
add an extra layer of security. The default
implementation doesn’t have a signature and every request end up being a query in the database for checking upon the
remember-me
token.
JWT
prevents that, although parsing it and validating its signature need some more CPU cycles.
@Configuration
public class AuthSecurityConfiguration extends SecurityConfigurationSupport {
...
@Bean
public UserDetailsService userDetailsService(UserService userService) {
return new DelegatingUserService(userService);
}
@Bean
public PersistentTokenRepository persistentTokenRepository(SessionService sessionService) {
return new DelegatingPersistentTokenRepository(sessionService);
}
@Bean
public RememberMeAuthenticationFilter rememberMeAuthenticationFilter(
AuthenticationManager authenticationManager, RememberMeServices rememberMeServices,
AuthenticationSuccessHandler authenticationSuccessHandler) {
RememberMeAuthenticationFilter rememberMeAuthenticationFilter =
new ProceedingRememberMeAuthenticationFilter(authenticationManager, rememberMeServices);
rememberMeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
return rememberMeAuthenticationFilter;
}
@Bean
public RememberMeServices rememberMeServices(
UserDetailsService userDetailsService, PersistentTokenRepository persistentTokenRepository) {
String secretKey = getRememberMeTokenSecretKey().orElseThrow(IllegalStateException::new);
return new PersistentJwtTokenBasedRememberMeServices(secretKey, userDetailsService, persistentTokenRepository);
}
...
@Override
protected void customizeRememberMe(HttpSecurity http) throws Exception {
UserDetailsService userDetailsService = lookup("userDetailsService");
PersistentTokenRepository persistentTokenRepository = lookup("persistentTokenRepository");
AbstractRememberMeServices rememberMeServices = lookup("rememberMeServices");
RememberMeAuthenticationFilter rememberMeAuthenticationFilter =
lookup("rememberMeAuthenticationFilter");
http.rememberMe()
.userDetailsService(userDetailsService)
.tokenRepository(persistentTokenRepository)
.rememberMeServices(rememberMeServices)
.key(rememberMeServices.getKey())
.and()
.logout()
.logoutUrl(LOGOUT_ENDPOINT)
.and()
.addFilterAt(rememberMeAuthenticationFilter, RememberMeAuthenticationFilter.class);
}
...
}
The magic is in the last part obviously. Basically, it’s about registering components with Spring Security and enable
remember-me services. What’s interesting though is that we need a key (a string) which used by
AbstractRememberMeServices
internally. AbstractRememberMeServices
is also the default logout handler in this setup and takes care of marking
tokens in the database as deleted upon logout.
remember-me
flag as JSON data in the body of POST requestBy default UsernamePasswordAuthenticationFilter
expects credentials as HTTP request parameters of a POST request, however we want to send a JSON document instead.
Further down the pipeline, AbstractRememberMeServices
also checks upon the existence of the remember-me flag as a request parameter.In order to fix that,
LoginFilter
set the remember-me
flag as a request attribute and delegates the decision to
PersistentTokenBasedRememberMeServices
if remember-me
authentication needs to be initiated or not.
RememberMeAuthenticationFilter doesn’t proceed to next filters in the filter chain, but it stops its execution if an AuthenticationSuccessHandler is set.
public class ProceedingRememberMeAuthenticationFilter extends RememberMeAuthenticationFilter {
private static final Logger LOGGER =
LoggerFactory.getLogger(ProceedingRememberMeAuthenticationFilter.class);
private AuthenticationSuccessHandler successHandler;
public ProceedingRememberMeAuthenticationFilter(
AuthenticationManager authenticationManager, RememberMeServices rememberMeServices) {
super(authenticationManager, rememberMeServices);
}
@Override
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
this.successHandler = successHandler;
}
@Override
protected void onSuccessfulAuthentication(
HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
if (successHandler == null) {
return;
}
try {
successHandler.onAuthenticationSuccess(request, response, authResult);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
}
}
ProceedingRememberMeAuthenticationFilter is a customized version of the original filter which doesn’t stop when authentication succeeds.
Building a user management microservice (Part 7): Putting it together
If you like Java and Spring as much as I do, sign up for my newsletter.