39 changed files with 1184 additions and 4 deletions
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
package gae; |
||||
|
||||
import com.google.appengine.tools.admin.AppCfg |
||||
import org.gradle.api.*; |
||||
|
||||
class GaePlugin implements Plugin<Project> { |
||||
public void apply(Project project) { |
||||
if (!project.hasProperty('appEngineSdkRoot')) { |
||||
println "'appEngineSdkRoot' must be set in gradle.properties" |
||||
} |
||||
|
||||
System.setProperty('appengine.sdk.root', project.property('appEngineSdkRoot')) |
||||
|
||||
File explodedWar = new File(project.buildDir, "gae-exploded") |
||||
|
||||
project.task('gaeDeploy') << { |
||||
AppCfg.main("update", explodedWar.toString()) |
||||
} |
||||
|
||||
project.gaeDeploy.dependsOn project.war |
||||
|
||||
project.war.doLast { |
||||
ant.unzip(src: project.war.archivePath, dest: explodedWar) |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
implementation-class=gae.GaePlugin |
||||
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
apply plugin: 'war' |
||||
apply plugin: 'jetty' |
||||
apply plugin: 'gae' |
||||
|
||||
gaeVersion="1.3.5" |
||||
|
||||
repositories { |
||||
// Hibernate Validator |
||||
mavenRepo name: 'JBoss', urls: 'https://repository.jboss.org/nexus/content/repositories/releases' |
||||
// GAE Jars |
||||
mavenRepo name: 'GAE', urls:'http://maven-gae-plugin.googlecode.com/svn/repository' |
||||
} |
||||
|
||||
// Remove logback as it causes security issues with GAE. |
||||
configurations.runtime.exclude(group: 'ch.qos.logback') |
||||
|
||||
dependencies { |
||||
providedCompile 'javax.servlet:servlet-api:2.5@jar', |
||||
"com.google.appengine:appengine-api-1.0-sdk:$gaeVersion" |
||||
|
||||
compile project(':spring-security-core'), |
||||
project(':spring-security-web'), |
||||
"org.springframework:spring-beans:$springVersion", |
||||
"org.springframework:spring-web:$springVersion", |
||||
"org.springframework:spring-webmvc:$springVersion", |
||||
"org.springframework:spring-context:$springVersion", |
||||
"org.springframework:spring-context-support:$springVersion", |
||||
'javax.validation:validation-api:1.0.0.GA', |
||||
'org.hibernate:hibernate-validator:4.1.0.Final', |
||||
"org.slf4j:slf4j-api:$slf4jVersion" |
||||
|
||||
runtime project(':spring-security-config'), |
||||
"org.slf4j:jcl-over-slf4j:$slf4jVersion", |
||||
"org.slf4j:slf4j-jdk14:$slf4jVersion" |
||||
testCompile "com.google.appengine:appengine-testing:$gaeVersion" |
||||
|
||||
testRuntime "com.google.appengine:appengine-api-labs:$gaeVersion", |
||||
"com.google.appengine:appengine-api-stubs:$gaeVersion" |
||||
|
||||
} |
||||
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
package samples.gae.security; |
||||
|
||||
import org.springframework.security.core.GrantedAuthority; |
||||
|
||||
/** |
||||
* @author Luke Taylor |
||||
*/ |
||||
public enum AppRole implements GrantedAuthority { |
||||
ADMIN (0), |
||||
NEW_USER (1), |
||||
USER (2); |
||||
|
||||
private int bit; |
||||
|
||||
/** |
||||
* Creates an authority with a specific bit representation. It's important that this doesn't |
||||
* change as it will be used in the database. The enum ordinal is less reliable as the enum may be |
||||
* reordered or have new roles inserted which would change the ordinal values. |
||||
* |
||||
* @param bit the permission bit which will represent this authority in the datastore. |
||||
*/ |
||||
AppRole(int bit) { |
||||
this.bit = bit; |
||||
} |
||||
|
||||
public int getBit() { |
||||
return bit; |
||||
} |
||||
|
||||
public String getAuthority() { |
||||
return toString(); |
||||
} |
||||
} |
||||
@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
|
||||
package samples.gae.security; |
||||
|
||||
import java.io.IOException; |
||||
import javax.servlet.FilterChain; |
||||
import javax.servlet.ServletException; |
||||
import javax.servlet.ServletRequest; |
||||
import javax.servlet.ServletResponse; |
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
import com.google.appengine.api.users.User; |
||||
import com.google.appengine.api.users.UserServiceFactory; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.security.authentication.AuthenticationDetailsSource; |
||||
import org.springframework.security.authentication.AuthenticationManager; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.core.context.SecurityContextHolder; |
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler; |
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; |
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; |
||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.web.filter.GenericFilterBean; |
||||
|
||||
/** |
||||
* @author Luke Taylor |
||||
*/ |
||||
public class GaeAuthenticationFilter extends GenericFilterBean { |
||||
private static final String REGISTRATION_URL = "/register.htm"; |
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass()); |
||||
|
||||
private AuthenticationDetailsSource ads = new WebAuthenticationDetailsSource(); |
||||
private AuthenticationManager authenticationManager; |
||||
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); |
||||
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { |
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); |
||||
|
||||
if (authentication == null) { |
||||
User googleUser = UserServiceFactory.getUserService().getCurrentUser(); |
||||
|
||||
if (googleUser != null) { |
||||
logger.debug("Currently logged on to GAE as user " + googleUser); |
||||
logger.debug("Authenticating to Spring Security"); |
||||
// User has returned after authenticating via GAE. Need to authenticate through Spring Security.
|
||||
PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(googleUser, null); |
||||
token.setDetails(ads.buildDetails(request)); |
||||
|
||||
try { |
||||
authentication = authenticationManager.authenticate(token); |
||||
SecurityContextHolder.getContext().setAuthentication(authentication); |
||||
} catch (AuthenticationException e) { |
||||
failureHandler.onAuthenticationFailure((HttpServletRequest)request, (HttpServletResponse)response, e); |
||||
|
||||
return; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// A new user has to register with the app before doing anything else
|
||||
if (authentication != null && authentication.getAuthorities().contains(AppRole.NEW_USER) |
||||
&& !((HttpServletRequest)request).getRequestURI().endsWith(REGISTRATION_URL)) { |
||||
logger.debug("New user authenticated. Redirecting to registration page"); |
||||
|
||||
((HttpServletResponse) response).sendRedirect(REGISTRATION_URL); |
||||
|
||||
return; |
||||
} |
||||
|
||||
chain.doFilter(request, response); |
||||
} |
||||
|
||||
@Override |
||||
public void afterPropertiesSet() throws ServletException { |
||||
Assert.notNull(authenticationManager, "AuthenticationManager must be set"); |
||||
} |
||||
|
||||
public void setAuthenticationManager(AuthenticationManager authenticationManager) { |
||||
this.authenticationManager = authenticationManager; |
||||
} |
||||
|
||||
public void setFailureHandler(AuthenticationFailureHandler failureHandler) { |
||||
this.failureHandler = failureHandler; |
||||
} |
||||
} |
||||
@ -0,0 +1,62 @@
@@ -0,0 +1,62 @@
|
||||
package samples.gae.security; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.HashSet; |
||||
|
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.GrantedAuthority; |
||||
import samples.gae.users.GaeUser; |
||||
|
||||
/** |
||||
* Authentication object representing a fully-authenticated user. |
||||
* |
||||
* @author Luke Taylor |
||||
*/ |
||||
public class GaeUserAuthentication implements Authentication { |
||||
private final GaeUser principal; |
||||
private final Object details; |
||||
private boolean authenticated; |
||||
|
||||
public GaeUserAuthentication(GaeUser principal, Object details) { |
||||
this.principal = principal; |
||||
this.details = details; |
||||
authenticated = true; |
||||
} |
||||
|
||||
public Collection<GrantedAuthority> getAuthorities() { |
||||
return new HashSet<GrantedAuthority>(principal.getAuthorities()); |
||||
} |
||||
|
||||
public Object getCredentials() { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
public Object getDetails() { |
||||
return null; |
||||
} |
||||
|
||||
public Object getPrincipal() { |
||||
return principal; |
||||
} |
||||
|
||||
public boolean isAuthenticated() { |
||||
return authenticated; |
||||
} |
||||
|
||||
public void setAuthenticated(boolean isAuthenticated) { |
||||
authenticated = isAuthenticated; |
||||
} |
||||
|
||||
public String getName() { |
||||
return principal.getUserId(); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "GaeUserAuthentication{" + |
||||
"principal=" + principal + |
||||
", details=" + details + |
||||
", authenticated=" + authenticated + |
||||
'}'; |
||||
} |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
package samples.gae.security; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import javax.servlet.ServletException; |
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.web.AuthenticationEntryPoint; |
||||
|
||||
import com.google.appengine.api.users.UserService; |
||||
import com.google.appengine.api.users.UserServiceFactory; |
||||
|
||||
public class GoogleAccountsAuthenticationEntryPoint implements AuthenticationEntryPoint { |
||||
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) |
||||
throws IOException, ServletException { |
||||
UserService userService = UserServiceFactory.getUserService(); |
||||
|
||||
response.sendRedirect(userService.createLoginURL(request.getRequestURI())); |
||||
} |
||||
} |
||||
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
package samples.gae.security; |
||||
|
||||
import com.google.appengine.api.users.User; |
||||
import org.springframework.context.MessageSource; |
||||
import org.springframework.context.MessageSourceAware; |
||||
import org.springframework.context.support.MessageSourceAccessor; |
||||
import org.springframework.security.authentication.AuthenticationProvider; |
||||
import org.springframework.security.authentication.DisabledException; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.core.SpringSecurityMessageSource; |
||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; |
||||
import samples.gae.users.GaeUser; |
||||
import samples.gae.users.UserRegistry; |
||||
|
||||
/** |
||||
* A simple authentication provider which interacts with {@code User} returned by the GAE {@code UserService}, |
||||
* and also the local persistent {@code UserRegistry} to build an application user principal. |
||||
* <p> |
||||
* If the user has been authenticated through google accounts, it will check if they are already registered |
||||
* and either load the existing user information or assign them a temporary identity with limited access until they |
||||
* have registered. |
||||
* <p> |
||||
* If the account has been disabled, a {@code DisabledException} will be raised. |
||||
* |
||||
* @author Luke Taylor |
||||
*/ |
||||
public class GoogleAccountsAuthenticationProvider implements AuthenticationProvider, MessageSourceAware { |
||||
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); |
||||
|
||||
private UserRegistry userRegistry; |
||||
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
||||
User googleUser = (User) authentication.getPrincipal(); |
||||
|
||||
GaeUser user = userRegistry.findUser(googleUser.getUserId()); |
||||
|
||||
if (user == null) { |
||||
// User not in registry. Needs to register
|
||||
user = new GaeUser(googleUser.getUserId(), googleUser.getNickname(), googleUser.getEmail()); |
||||
} |
||||
|
||||
if (!user.isEnabled()) { |
||||
throw new DisabledException("Account is disabled"); |
||||
} |
||||
|
||||
return new GaeUserAuthentication(user, authentication.getDetails()); |
||||
} |
||||
|
||||
/** |
||||
* Indicate that this provider only supports PreAuthenticatedAuthenticationToken (sub)classes. |
||||
*/ |
||||
public final boolean supports(Class<?> authentication) { |
||||
return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication); |
||||
} |
||||
|
||||
public void setUserRegistry(UserRegistry userRegistry) { |
||||
this.userRegistry = userRegistry; |
||||
} |
||||
|
||||
public void setMessageSource(MessageSource messageSource) { |
||||
this.messages = new MessageSourceAccessor(messageSource); |
||||
} |
||||
} |
||||
@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
|
||||
package samples.gae.users; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.EnumSet; |
||||
import java.util.Set; |
||||
|
||||
import com.google.appengine.api.datastore.*; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.security.core.GrantedAuthority; |
||||
import samples.gae.security.AppRole; |
||||
|
||||
/** |
||||
* UserRegistry implementation which uses GAE's low-level Datastore APIs. |
||||
* |
||||
* @author Luke Taylor |
||||
*/ |
||||
public class GaeDataStoreUserRegistry implements UserRegistry { |
||||
private final Logger logger = LoggerFactory.getLogger(getClass()); |
||||
|
||||
private static final String USER_TYPE = "GaeUser"; |
||||
private static final String USER_FORENAME = "forename"; |
||||
private static final String USER_SURNAME = "surname"; |
||||
private static final String USER_NICKNAME = "nickname"; |
||||
private static final String USER_EMAIL = "email"; |
||||
private static final String USER_ENABLED = "enabled"; |
||||
private static final String USER_AUTHORITIES = "authorities"; |
||||
|
||||
public GaeUser findUser(String userId) { |
||||
Key key = KeyFactory.createKey(USER_TYPE, userId); |
||||
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); |
||||
|
||||
try { |
||||
Entity user = datastore.get(key); |
||||
|
||||
long binaryAuthorities = (Long)user.getProperty(USER_AUTHORITIES); |
||||
Set<AppRole> roles = EnumSet.noneOf(AppRole.class); |
||||
|
||||
for (AppRole r : AppRole.values()) { |
||||
if ((binaryAuthorities & (1 << r.getBit())) != 0) { |
||||
roles.add(r); |
||||
} |
||||
} |
||||
|
||||
GaeUser gaeUser = new GaeUser( |
||||
user.getKey().getName(), |
||||
(String)user.getProperty(USER_NICKNAME), |
||||
(String)user.getProperty(USER_EMAIL), |
||||
(String)user.getProperty(USER_FORENAME), |
||||
(String)user.getProperty(USER_SURNAME), |
||||
roles, |
||||
(Boolean)user.getProperty(USER_ENABLED)); |
||||
|
||||
return gaeUser; |
||||
|
||||
} catch (EntityNotFoundException e) { |
||||
logger.debug(userId + " not found in datastore"); |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
public void registerUser(GaeUser newUser) { |
||||
logger.debug("Attempting to create new user " + newUser); |
||||
|
||||
Key key = KeyFactory.createKey(USER_TYPE, newUser.getUserId()); |
||||
Entity user = new Entity(key); |
||||
user.setProperty(USER_EMAIL, newUser.getEmail()); |
||||
user.setProperty(USER_NICKNAME, newUser.getNickname()); |
||||
user.setProperty(USER_FORENAME, newUser.getForename()); |
||||
user.setProperty(USER_SURNAME, newUser.getSurname()); |
||||
user.setUnindexedProperty(USER_ENABLED, newUser.isEnabled()); |
||||
|
||||
Collection<? extends GrantedAuthority> roles = newUser.getAuthorities(); |
||||
|
||||
long binaryAuthorities = 0; |
||||
|
||||
for (GrantedAuthority r : roles) { |
||||
binaryAuthorities |= 1 << ((AppRole)r).getBit(); |
||||
} |
||||
|
||||
user.setUnindexedProperty(USER_AUTHORITIES, binaryAuthorities); |
||||
|
||||
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); |
||||
datastore.put(user); |
||||
} |
||||
|
||||
public void removeUser(String userId) { |
||||
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); |
||||
Key key = KeyFactory.createKey(USER_TYPE, userId); |
||||
|
||||
datastore.delete(key); |
||||
} |
||||
} |
||||
@ -0,0 +1,92 @@
@@ -0,0 +1,92 @@
|
||||
package samples.gae.users; |
||||
|
||||
import java.io.Serializable; |
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
import java.util.EnumSet; |
||||
import java.util.Set; |
||||
|
||||
import org.springframework.security.core.GrantedAuthority; |
||||
import samples.gae.security.AppRole; |
||||
|
||||
/** |
||||
* Custom user object for the application. |
||||
* |
||||
* @author Luke Taylor |
||||
*/ |
||||
public class GaeUser implements Serializable { |
||||
private final String userId; |
||||
private final String email; |
||||
private final String nickname; |
||||
private final String forename; |
||||
private final String surname; |
||||
private final Set<AppRole> authorities; |
||||
private final boolean enabled; |
||||
|
||||
/** |
||||
* Pre-registration constructor. |
||||
* |
||||
* Assigns the user the "NEW_USER" role only. |
||||
*/ |
||||
public GaeUser(String userId, String nickname, String email) { |
||||
this.userId = userId; |
||||
this.nickname = nickname; |
||||
this.authorities = EnumSet.of(AppRole.NEW_USER); |
||||
this.forename = null; |
||||
this.surname = null; |
||||
this.email = email; |
||||
this.enabled = true; |
||||
} |
||||
|
||||
/** |
||||
* Post-registration constructor |
||||
*/ |
||||
public GaeUser(String userId, String nickname, String email, String forename, String surname, Set<AppRole> authorities, boolean enabled) { |
||||
this.userId = userId; |
||||
this.nickname = nickname; |
||||
this.email = email; |
||||
this.authorities = authorities; |
||||
this.forename = forename; |
||||
this.surname = surname; |
||||
this.enabled= enabled; |
||||
} |
||||
|
||||
public String getUserId() { |
||||
return userId; |
||||
} |
||||
|
||||
public String getNickname() { |
||||
return nickname; |
||||
} |
||||
|
||||
public String getEmail() { |
||||
return email; |
||||
} |
||||
|
||||
public String getForename() { |
||||
return forename; |
||||
} |
||||
|
||||
public String getSurname() { |
||||
return surname; |
||||
} |
||||
|
||||
public boolean isEnabled() { |
||||
return enabled; |
||||
} |
||||
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() { |
||||
return authorities; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "GaeUser{" + |
||||
"userId='" + userId + '\'' + |
||||
", nickname='" + nickname + '\'' + |
||||
", forename='" + forename + '\'' + |
||||
", surname='" + surname + '\'' + |
||||
", authorities=" + authorities + |
||||
'}'; |
||||
} |
||||
} |
||||
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
package samples.gae.users; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* @author Luke Taylor |
||||
*/ |
||||
public class InMemoryUserRegistry implements UserRegistry { |
||||
private final Logger logger = LoggerFactory.getLogger(getClass()); |
||||
|
||||
private Map<String, GaeUser> users = Collections.synchronizedMap(new HashMap<String, GaeUser>()); |
||||
|
||||
public GaeUser findUser(String userId) { |
||||
return users.get(userId); |
||||
} |
||||
|
||||
public void registerUser(GaeUser newUser) { |
||||
logger.debug("Attempting to create new user " + newUser); |
||||
|
||||
Assert.state(!users.containsKey(newUser.getUserId())); |
||||
|
||||
users.put(newUser.getUserId(), newUser); |
||||
} |
||||
|
||||
public void removeUser(String userId) { |
||||
users.remove(userId); |
||||
} |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
package samples.gae.users; |
||||
|
||||
import com.google.appengine.api.datastore.EntityNotFoundException; |
||||
|
||||
/** |
||||
* |
||||
* Service used to maintain a list of users who are registered with the application. |
||||
* |
||||
* @author Luke Taylor |
||||
*/ |
||||
public interface UserRegistry { |
||||
|
||||
GaeUser findUser(String userId); |
||||
|
||||
void registerUser(GaeUser newUser); |
||||
|
||||
void removeUser(String userId); |
||||
} |
||||
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
package samples.gae.validation; |
||||
|
||||
import static java.lang.annotation.ElementType.*; |
||||
|
||||
import java.lang.annotation.Retention; |
||||
import java.lang.annotation.RetentionPolicy; |
||||
import java.lang.annotation.Target; |
||||
import javax.validation.Constraint; |
||||
import javax.validation.Payload; |
||||
|
||||
import org.hibernate.validator.constraints.NotBlank; |
||||
|
||||
/** |
||||
* @author Luke Taylor |
||||
*/ |
||||
@Target( { METHOD, FIELD, ANNOTATION_TYPE }) |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Constraint(validatedBy = ForenameValidator.class) |
||||
public @interface Forename { |
||||
String message() default "{samples.gae.forename}"; |
||||
|
||||
Class<?>[] groups() default {}; |
||||
|
||||
Class<? extends Payload>[] payload() default {}; |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
package samples.gae.validation; |
||||
|
||||
import java.util.regex.Pattern; |
||||
import javax.validation.ConstraintValidator; |
||||
import javax.validation.ConstraintValidatorContext; |
||||
|
||||
/** |
||||
* @author Luke Taylor |
||||
*/ |
||||
public class ForenameValidator implements ConstraintValidator<Forename, String> { |
||||
private static final Pattern VALID = Pattern.compile("[\\p{L}'\\-,.]+") ; |
||||
|
||||
public void initialize(Forename constraintAnnotation) {} |
||||
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) { |
||||
return VALID.matcher(value).matches(); |
||||
} |
||||
} |
||||
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
package samples.gae.validation; |
||||
|
||||
import static java.lang.annotation.ElementType.*; |
||||
|
||||
import java.lang.annotation.Retention; |
||||
import java.lang.annotation.RetentionPolicy; |
||||
import java.lang.annotation.Target; |
||||
import javax.validation.Constraint; |
||||
import javax.validation.Payload; |
||||
|
||||
import org.hibernate.validator.constraints.NotBlank; |
||||
|
||||
/** |
||||
* @author Luke Taylor |
||||
*/ |
||||
@Target( { METHOD, FIELD, ANNOTATION_TYPE }) |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Constraint(validatedBy = SurnameValidator.class) |
||||
public @interface Surname { |
||||
String message() default "{samples.gae.surname}"; |
||||
|
||||
Class<?>[] groups() default {}; |
||||
|
||||
Class<? extends Payload>[] payload() default {}; |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
package samples.gae.validation; |
||||
|
||||
import java.util.regex.Pattern; |
||||
import javax.validation.ConstraintValidator; |
||||
import javax.validation.ConstraintValidatorContext; |
||||
|
||||
/** |
||||
* @author Luke Taylor |
||||
*/ |
||||
public class SurnameValidator implements ConstraintValidator<Surname, String> { |
||||
private static final Pattern VALID = Pattern.compile("[\\p{L}'\\-,.]+") ; |
||||
|
||||
public void initialize(Surname constraintAnnotation) {} |
||||
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) { |
||||
return VALID.matcher(value).matches(); |
||||
} |
||||
} |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
package samples.gae.web; |
||||
|
||||
import java.io.IOException; |
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
import com.google.appengine.api.users.UserServiceFactory; |
||||
import org.springframework.stereotype.Controller; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.RequestMethod; |
||||
|
||||
/** |
||||
* |
||||
* @author Luke Taylor |
||||
* |
||||
*/ |
||||
@Controller |
||||
public class GaeAppController { |
||||
|
||||
@RequestMapping(value = "/", method= RequestMethod.GET) |
||||
public String landing() { |
||||
return "landing"; |
||||
} |
||||
|
||||
@RequestMapping(value = "/home.htm", method= RequestMethod.GET) |
||||
public String home() { |
||||
return "home"; |
||||
} |
||||
|
||||
@RequestMapping(value = "/disabled.htm", method= RequestMethod.GET) |
||||
public String disabled() { |
||||
return "disabled"; |
||||
} |
||||
|
||||
@RequestMapping(value = "/logout.htm", method= RequestMethod.GET) |
||||
public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException { |
||||
request.getSession().invalidate(); |
||||
|
||||
String logoutUrl = UserServiceFactory.getUserService().createLogoutURL("/"); |
||||
|
||||
response.sendRedirect(logoutUrl); |
||||
} |
||||
|
||||
@RequestMapping(value = "/loggedout.htm", method= RequestMethod.GET) |
||||
public String loggedOut() { |
||||
return "loggedout"; |
||||
} |
||||
} |
||||
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
package samples.gae.web; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.EnumSet; |
||||
import java.util.Set; |
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.validation.Valid; |
||||
|
||||
import com.google.appengine.api.users.UserServiceFactory; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.context.SecurityContextHolder; |
||||
import org.springframework.stereotype.Controller; |
||||
import org.springframework.validation.BindingResult; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.RequestMethod; |
||||
import samples.gae.security.AppRole; |
||||
import samples.gae.security.GaeUserAuthentication; |
||||
import samples.gae.users.GaeUser; |
||||
import samples.gae.users.UserRegistry; |
||||
|
||||
/** |
||||
* @author Luke Taylor |
||||
*/ |
||||
@Controller |
||||
@RequestMapping(value="/register.htm") |
||||
public class RegistrationController { |
||||
|
||||
@Autowired |
||||
private UserRegistry registry; |
||||
|
||||
@RequestMapping(method= RequestMethod.GET) |
||||
public RegistrationForm registrationForm() { |
||||
return new RegistrationForm(); |
||||
} |
||||
|
||||
@RequestMapping(method = RequestMethod.POST) |
||||
public String register(@Valid RegistrationForm form, BindingResult result) { |
||||
if (result.hasErrors()) { |
||||
return null; |
||||
} |
||||
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); |
||||
GaeUser currentUser = (GaeUser)authentication.getPrincipal(); |
||||
Set<AppRole> roles = EnumSet.of(AppRole.USER); |
||||
|
||||
if (UserServiceFactory.getUserService().isUserAdmin()) { |
||||
roles.add(AppRole.ADMIN); |
||||
} |
||||
|
||||
GaeUser user = new GaeUser(currentUser.getUserId(), currentUser.getNickname(), currentUser.getEmail(), |
||||
form.getForename(), form.getSurname(), roles, true); |
||||
|
||||
registry.registerUser(user); |
||||
|
||||
// Update the context with the full authentication
|
||||
SecurityContextHolder.getContext().setAuthentication(new GaeUserAuthentication(user, authentication.getDetails())); |
||||
|
||||
return "redirect:/home.htm"; |
||||
} |
||||
} |
||||
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
package samples.gae.web; |
||||
|
||||
import org.hibernate.validator.constraints.NotBlank; |
||||
import samples.gae.validation.Forename; |
||||
import samples.gae.validation.Surname; |
||||
|
||||
/** |
||||
* @author Luke Taylor |
||||
*/ |
||||
public class RegistrationForm { |
||||
@Forename |
||||
private String forename; |
||||
@Surname |
||||
private String surname; |
||||
|
||||
public String getForename() { |
||||
return forename; |
||||
} |
||||
|
||||
public void setForename(String forename) { |
||||
this.forename = forename; |
||||
} |
||||
|
||||
public String getSurname() { |
||||
return surname; |
||||
} |
||||
|
||||
public void setSurname(String surname) { |
||||
this.surname = surname; |
||||
} |
||||
} |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0"> |
||||
<application>gaespringsec</application> |
||||
<version>1</version> |
||||
<sessions-enabled>true</sessions-enabled> |
||||
|
||||
<system-properties> |
||||
<property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/> |
||||
</system-properties> |
||||
</appengine-web-app> |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
||||
<b:beans xmlns="http://www.springframework.org/schema/security" |
||||
xmlns:b="http://www.springframework.org/schema/beans" |
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd |
||||
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> |
||||
|
||||
<http pattern="/static/**" security="none" /> |
||||
<http pattern="/favicon.ico" security="none" /> |
||||
<http pattern="/_ah/resources**" security="none" /> |
||||
|
||||
<http use-expressions="true" entry-point-ref="gaeEntryPoint"> |
||||
<intercept-url pattern="/" access="permitAll" /> |
||||
<intercept-url pattern="/_ah/login**" access="permitAll" /> |
||||
<intercept-url pattern="/_ah/admin**" access="permitAll" /> |
||||
<intercept-url pattern="/logout.htm" access="permitAll" /> |
||||
<intercept-url pattern="/register.htm*" access="hasRole('NEW_USER')" /> |
||||
<intercept-url pattern="/**" access="hasRole('USER')" /> |
||||
<custom-filter position="PRE_AUTH_FILTER" ref="gaeFilter" /> |
||||
</http> |
||||
|
||||
<b:bean id="gaeEntryPoint" class="samples.gae.security.GoogleAccountsAuthenticationEntryPoint" /> |
||||
|
||||
<b:bean id="gaeFilter" class="samples.gae.security.GaeAuthenticationFilter"> |
||||
<b:property name="authenticationManager" ref="authenticationManager"/> |
||||
<b:property name="failureHandler"> |
||||
<b:bean class="org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler"> |
||||
<b:property name="exceptionMappings"> |
||||
<b:map> |
||||
<b:entry key="org.springframework.security.authentication.DisabledException" value="/disabled.htm" /> |
||||
</b:map> |
||||
</b:property> |
||||
</b:bean> |
||||
</b:property> |
||||
</b:bean> |
||||
|
||||
<authentication-manager alias="authenticationManager"> |
||||
<authentication-provider ref="gaeAuthenticationProvider"/> |
||||
</authentication-manager> |
||||
|
||||
<b:bean id="gaeAuthenticationProvider" class="samples.gae.security.GoogleAccountsAuthenticationProvider"> |
||||
<b:property name="userRegistry" ref="userRegistry" /> |
||||
</b:bean> |
||||
|
||||
<b:bean id="userRegistry" class="samples.gae.users.GaeDataStoreUserRegistry" /> |
||||
|
||||
</b:beans> |
||||
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
samples.gae.forename=Must be a valid forename |
||||
samples.gae.surname=Must be a valid surname |
||||
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" |
||||
xmlns:mvc="http://www.springframework.org/schema/mvc" |
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd |
||||
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd |
||||
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd"> |
||||
|
||||
<!-- Enables JSR-303 --> |
||||
<mvc:annotation-driven/> |
||||
|
||||
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" /> |
||||
|
||||
<context:component-scan base-package="samples.gae"/> |
||||
<context:annotation-config /> |
||||
|
||||
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> |
||||
<property name="basename" value="messages"/> |
||||
</bean> |
||||
|
||||
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> |
||||
<property name="prefix" value="/WEB-INF/jsp/"/> |
||||
<property name="suffix" value=".jsp"/> |
||||
</bean> |
||||
|
||||
</beans> |
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %> |
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> |
||||
<%@page session="false" %> |
||||
|
||||
<html> |
||||
<head> |
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8"> |
||||
<link rel="stylesheet" href="/static/css/gae.css" type="text/css" /> |
||||
<title>Disabled Account</title></head> |
||||
<body> |
||||
<div id="content"> |
||||
<p>Sorry, it looks like your account has been disabled for some reason...</p> |
||||
</div> |
||||
</body> |
||||
|
||||
</html> |
||||
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
<%@ page import="com.google.appengine.api.users.UserServiceFactory" %> |
||||
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> |
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %> |
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> |
||||
|
||||
<html> |
||||
<head> |
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8"> |
||||
<link rel="stylesheet" href="/static/css/gae.css" type="text/css" /> |
||||
<title>Home Page</title> |
||||
</head> |
||||
<body> |
||||
<div id="content"> |
||||
<h3>The Home Page</h3> |
||||
<p>Welcome back <sec:authentication property="principal.nickname"/>.</p> |
||||
<p> |
||||
You can get to this page if you have authenticated and are a registered user. |
||||
You are registered as |
||||
<sec:authentication property="principal.forename"/> <sec:authentication property="principal.surname"/>. |
||||
</p> |
||||
<p> |
||||
<a href="/logout.htm">Logout</a>. |
||||
</p> |
||||
</div> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %> |
||||
<%@page session="false" %> |
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> |
||||
|
||||
<html> |
||||
<head> |
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8"> |
||||
<link rel="stylesheet" href="/static/css/gae.css" type="text/css" /> |
||||
<title>Spring Security GAE Sample</title> |
||||
</head> |
||||
|
||||
<body> |
||||
<div id="content"> |
||||
<h3>Spring Security GAE Application</h3> |
||||
|
||||
<p> |
||||
This application demonstrates the integration of Spring Security |
||||
with the services provided by Google App Engine. It shows how to: |
||||
<ul> |
||||
<li>Authenticate using Google Accounts.</li> |
||||
<li>Implement "on–demand" authentication when a user accesses a secured resource.</li> |
||||
<li>Supplement the information from Google Accounts with application–specific roles.</li> |
||||
<li>Store user account data in an App Engine datastore using the native API.</li> |
||||
<li>Setup access-control restrictions based on the roles assigned to users.</li> |
||||
<li>Disable the accounts of specfic users to prevent access.</li> |
||||
</ul> |
||||
</p> |
||||
<p> |
||||
Go to the <a href="/home.htm">home page</a>. |
||||
</p> |
||||
</div> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %> |
||||
<%@page session="false" %> |
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> |
||||
|
||||
<html> |
||||
<head> |
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8"> |
||||
<link rel="stylesheet" href="/static/css/gae.css" type="text/css" /> |
||||
<title>Spring Security GAE Sample</title> |
||||
</head> |
||||
|
||||
<body> |
||||
<div id="content"> |
||||
<p>You've been logged out of the application. <a href="/home.htm">Log back in</a>.</p> |
||||
</div> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> |
||||
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> |
||||
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> |
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %> |
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> |
||||
<html> |
||||
<head> |
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8"> |
||||
<link rel="stylesheet" href="/static/css/gae.css" type="text/css" /> |
||||
<title>Registration</title> |
||||
</head> |
||||
<body> |
||||
<div id="content"> |
||||
<p> |
||||
Welcome to the Spring Security GAE sample application, <sec:authentication property="principal.nickname" />. |
||||
Please enter your registration details in order to use the application. |
||||
</p> |
||||
<p> |
||||
The data you enter here will be registered in the application's GAE data store, keyed under your unique |
||||
Google Accounts identifier. It doesn't have to be accurate. When you log in again, the information will be automatically |
||||
retrieved. |
||||
</p> |
||||
|
||||
<form:form id="register" method="post" modelAttribute="registrationForm"> |
||||
<fieldset> |
||||
<form:label path="forename"> |
||||
Forename: |
||||
</form:label> <form:errors path="forename" cssClass="fieldError" /><br /> |
||||
<form:input path="forename" /> <br /> |
||||
|
||||
<form:label path="surname"> |
||||
Surname: |
||||
</form:label><form:errors path="surname" cssClass="fieldError" /> <br /> |
||||
<form:input path="surname" /><br /> |
||||
</fieldset> |
||||
<input type="submit" value="Register"> |
||||
</form:form> |
||||
</body> |
||||
</div> |
||||
</html> |
||||
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
.level = INFO |
||||
|
||||
org.springframework.level = FINE |
||||
org.springframework.security.level = FINER |
||||
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" |
||||
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" |
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> |
||||
|
||||
<context-param> |
||||
<param-name>contextConfigLocation</param-name> |
||||
<param-value> |
||||
/WEB-INF/applicationContext-security.xml |
||||
</param-value> |
||||
</context-param> |
||||
|
||||
<filter> |
||||
<filter-name>springSecurityFilterChain</filter-name> |
||||
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> |
||||
</filter> |
||||
|
||||
<filter-mapping> |
||||
<filter-name>springSecurityFilterChain</filter-name> |
||||
<url-pattern>/*</url-pattern> |
||||
</filter-mapping> |
||||
|
||||
<listener> |
||||
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> |
||||
</listener> |
||||
|
||||
<servlet> |
||||
<servlet-name>gae</servlet-name> |
||||
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> |
||||
<init-param> |
||||
<param-name>contextConfigLocation</param-name> |
||||
<param-value>/WEB-INF/gae-servlet.xml</param-value> |
||||
</init-param> |
||||
</servlet> |
||||
|
||||
<servlet-mapping> |
||||
<servlet-name>gae</servlet-name> |
||||
<url-pattern>/</url-pattern> |
||||
</servlet-mapping> |
||||
|
||||
<servlet-mapping> |
||||
<servlet-name>gae</servlet-name> |
||||
<url-pattern>*.htm</url-pattern> |
||||
</servlet-mapping> |
||||
|
||||
</web-app> |
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
|
||||
body { |
||||
font-family:"Palatino Linotype","Book Antiqua",Palatino,serif; |
||||
} |
||||
|
||||
#content { |
||||
margin: 5em auto; |
||||
width: 40em; |
||||
} |
||||
|
||||
form { |
||||
width: 25em; |
||||
margin: 0 2em; |
||||
} |
||||
|
||||
form fieldset { |
||||
margin-bottom: 0.5em; |
||||
} |
||||
|
||||
fieldset input { |
||||
margin: 0.6em 0; |
||||
} |
||||
|
||||
.fieldError { |
||||
color: red; |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
package samples.gae.security; |
||||
|
||||
import static org.junit.Assert.*; |
||||
import static samples.gae.security.AppRole.*; |
||||
|
||||
import org.junit.Test; |
||||
import org.springframework.security.core.GrantedAuthority; |
||||
|
||||
/** |
||||
* @author Luke Taylor |
||||
*/ |
||||
public class AppRoleTests { |
||||
|
||||
@Test |
||||
public void getAuthorityReturnsRoleName() { |
||||
GrantedAuthority admin = ADMIN; |
||||
|
||||
assertEquals("ADMIN", admin.getAuthority()); |
||||
} |
||||
|
||||
|
||||
|
||||
} |
||||
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
package samples.gae.users; |
||||
|
||||
import static org.junit.Assert.assertEquals; |
||||
|
||||
import java.util.EnumSet; |
||||
import java.util.Set; |
||||
|
||||
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; |
||||
import com.google.appengine.tools.development.testing.LocalServiceTestHelper; |
||||
import org.junit.After; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import samples.gae.security.AppRole; |
||||
|
||||
/** |
||||
* @author Luke Taylor |
||||
*/ |
||||
public class GaeDataStoreUserRegistryTests { |
||||
private final LocalServiceTestHelper helper = |
||||
new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig()); |
||||
|
||||
@Before |
||||
public void setUp() throws Exception { |
||||
helper.setUp(); |
||||
} |
||||
|
||||
@After |
||||
public void tearDown() throws Exception { |
||||
helper.tearDown(); |
||||
} |
||||
|
||||
@Test |
||||
public void correctDataIsRetrievedAfterInsert() { |
||||
GaeDataStoreUserRegistry registry = new GaeDataStoreUserRegistry(); |
||||
|
||||
Set<AppRole> roles = EnumSet.of(AppRole.ADMIN, AppRole.USER); |
||||
String userId = "someUserId"; |
||||
|
||||
GaeUser origUser = new GaeUser(userId, "nick", "nick@blah.com", "Forename", "Surname", roles, true); |
||||
|
||||
registry.registerUser(origUser); |
||||
|
||||
GaeUser loadedUser = registry.findUser(userId); |
||||
|
||||
assertEquals(loadedUser.getUserId(), origUser.getUserId()); |
||||
assertEquals(true, loadedUser.isEnabled()); |
||||
assertEquals(roles, loadedUser.getAuthorities()); |
||||
assertEquals("nick", loadedUser.getNickname()); |
||||
assertEquals("nick@blah.com", loadedUser.getEmail()); |
||||
assertEquals("Forename", loadedUser.getForename()); |
||||
assertEquals("Surname", loadedUser.getSurname()); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue