Conteng Evolved

Stuff, mostly software development stuff

Remember-Me Authentication With Spring Security on Google App Engine

Spring Security has built-in “remember-me” authentication capability to remember the identity of a user (principle) between session.

However, default implementations for “persistent token” only support in-memory (for testing) and JDBC. I wanted to implement this with App Engine using Objectify for persistence.

All it takes is to implement a custom PersistentTokenRepository and configure Spring Security to use it. This post assumes that Spring Security has been configured to work correctly with App Engine.

RememberMeToken Entity

Define an Objectify entity class that will be used to store token data.

1
2
3
4
5
6
7
8
9
10
11
@Entity
public class RememberMeToken {
    @Id
    private String series;
    @Index
    private String username;
    private String tokenValue;
    private Date date;

    // getters/setters
}

Converting Between PersistentRememberMeToken And RememberMeToken

Since different objects are used during runtime and persistence, I use Spring’s generic conversion service to help with type conversion.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class RememberMeConverter implements GenericConverter {
    @Override
    public Set<ConvertiblePair> getConvertibleTypes() {
        return of(
                new ConvertiblePair(RememberMeToken.class, PersistentRememberMeToken.class),
                new ConvertiblePair(PersistentRememberMeToken.class, RememberMeToken.class)
        );
    }

    @Override
    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
        if (sourceType.getType().equals(RememberMeToken.class)) {
            // RememberMeToken to PersistentRememberMeToken

            RememberMeToken from = (RememberMeToken) source;
            return new PersistentRememberMeToken(from.getUsername(), from.getSeries(), from.getTokenValue(), from.getDate());

        } else {
            // PersistentRememberMeToken to RememberMeToken

            PersistentRememberMeToken from = (PersistentRememberMeToken) source;
            return new RememberMeToken(from.getUsername(), from.getSeries(), from.getTokenValue(), from.getDate());
        }
    }
}

Create ObjectifyPersistentTokenRepository as a PersistentTokenRepository

This PersistentTokenRepository implementation uses a conversion service that has a RememberMeConverter registered and Objectify for persistence.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ObjectifyPersistentTokenRepository implements PersistentTokenRepository {
    private ConversionService conversionService;

    public ObjectifyPersistentTokenRepository(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    @Override
    public void createNewToken(PersistentRememberMeToken token) {
        RememberMeToken current = ofy().load().type(RememberMeToken.class).id(token.getSeries()).now();
        if (current != null) {
            throw new DataIntegrityViolationException("Series Id '" + token.getSeries() + "' already exists!");
        }

        RememberMeToken ofyToken = conversionService.convert(token, RememberMeToken.class);
        ofy().save().entity(ofyToken);
    }

    @Override
    public void updateToken(String series, String tokenValue, Date lastUsed) {
        PersistentRememberMeToken token = getTokenForSeries(series);
        RememberMeToken ofyToken = new RememberMeToken(token.getUsername(), series, tokenValue, new Date());
        ofy().save().entity(ofyToken);
    }

    @Override
    public PersistentRememberMeToken getTokenForSeries(String seriesId) {
        RememberMeToken rememberMeToken = ofy().load().type(RememberMeToken.class).id(seriesId).now();
        if (rememberMeToken != null) {
            return conversionService.convert(rememberMeToken, PersistentRememberMeToken.class);
        } else {
            return null;
        }
    }

    @Override
    public void removeUserTokens(String username) {
        ofy().delete().keys(
                ofy().load().type(RememberMeToken.class).filter("username", username).keys().list()
        );
    }
}

Spring Security Configuration

The final bit is to hook things up when configuring Spring Security. This can be done in the configure(HttpSecurity http) method if you are using Java configuration.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private ConversionService conversionService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // common configuration
            .authorizeRequests()
            .antMatchers("/").permitAll()
            .anyRequest().authenticated()
            .and().formLogin().loginPage("/login").permitAll()
            .and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).logoutSuccessUrl("/").permitAll()

            // use the custom persistent token repository
            .and().rememberMe().tokenRepository(new ObjectifyPersistentTokenRepository(conversionService));

    }
}

JSP Login Page

Just in case you are wondering how the HTML form fields look:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<form action="${loginUrl}" method="POST">
    <c:if test="${param.error != null}">
        <p>
            Invalid username and password.
        </p>
    </c:if>
    <c:if test="${param.logout != null}">
        <p>
            You have been logged out.
        </p>
    </c:if>
    <p>
        <label for="username">Username</label>
        <input type="text" id="username" name="username"/>
    </p>

    <p>
        <label for="password">Password</label>
        <input type="password" id="password" name="password"/>
    </p>
    <p>
        <input type="checkbox" name="remember-me" value="true"> Remember Me
    </p>
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
    <button type="submit" class="btn">Log in</button>
</form>

Comments