- Java 8+
- Spring Boot 1.5.4
- Spring data jpa for persistence
- Maven 3.0+
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring-security.version}</version>
</dependency>
security.user.name=user
security.user.password=password
security.basic.authorize-mode=authenticated
security.basic.path=/**
- above enable basic authentication
@EnableWebSecurity
public class BasicSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password")
.roles("USER_ADMIN")
;
}
}
- change the global configuration
- disable basic auth config
- replace basic auth with default login form config (auto generated in this case bc default configure method in WebSecurityConfigurerAdapter)
- is possible to override auto generated form
- Override method
configure
fromWebSecurityConfigurerAdapter
- Difference btw use of Role and Authority in url authorization:
- hasRole("ADMIN") looks for
ROLE_
prefix authority (so it really checks forROLE_ADMIN
authority) - hasAuthority("ADMIN") looks for ADMIN, so
Authority
API don't looks for prefix, is new and clean
- hasRole("ADMIN") looks for
- Url authorization goes from specific (delete) to general (anyRequest)
- If a user that try to access an URL secured and don't have authority then a 403 status code is send by API
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/delete/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin() // this is default login created by spring when no overriding configure method
;
}
hasAuthority
: is the principal authorityhasAnyRole
: can have any role configured (ROLE_ADMIN, ROLE_ROOT)hasAnyAuthority
: can have any of authorities passed (ADMIN, ROOT)hasIpAddress
: not very used in production, useful to be able to pinpoint a specific ip addressaccess
: allow the use of expressionsauthenticated
: just need to be authenticated in order to use url, no special authority or privilege just authenticatedanonymous
: any type of access is ok for urldenyAll
: restrict any kind of accesspermitAll
fullyAuthenticated
, rememberMe: are tiednot
: allow chaining
- Configured in method
configure
(overrid) ofWebSecurityConfigurerAdapter
- Default login form page is /login also the processing url page is /login
- Reason for not wanting to use the default configuration is that the defaults basically leak implementation details
- When using defaults other people can know about the framework and can exploit vulnerabilities if not patched
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
//.antMatchers("/delete/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll() // login form page, exception to be available for people not logged in
.loginProcessingUrl("/doLogin") // login proccesion url where authentication happens
.and()
.csrf().disable()
;
}
- Create login page (thymeleaf) and reference it with a controller
@RequestMapping("/login")
public String list() {
return "loginPage";
}
- Logout url default is /logout so it needs to change
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/doLogout", "GET"))
allow to be stricter and specify exact http method to do logout- if CSRF is enabled, GET won’t work for logging out, only POST
- only POST must be used, since logout is an operation that changes the state of the system
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll() // login form page, exception to be available for people not logged in
.loginProcessingUrl("/doLogin") // login proccesing url where authentication happens
.and()
.logout()
.permitAll().logoutUrl("/logout") // logout processing page
//.clearAuthentication()
//.deleteCookies()
//.invalidateHttpSession()
//.logoutSuccessHandler()
//.logoutSuccessUrl()
.and()
.csrf().disable()
;
}
clearAuthentication
: is true by default, but can be turn it off. Typically, that's not something wanted, but there are production scenarios where you might need to make sure that you don't clear authentication when your user logs outdeleteCookies
: nice way to specify that when your user logs out, a list of custom cookies should be cleared. When using custom cookies that do need to be cleared on logout, this is the way to do itinvalidateHttpSession
: enabled by default, it's something that you can change if you have a scenario that requires you to not invalidate the session when your user logs outlogoutSuccessUrl
: when logged out, we were automatically redirected to the login page, with an extra logout parameter. You may want to have a custom logout page saying you have been logged out, and maybe presenting some extra information. So if you need the logout process to redirect to a different page, not the login page, this is the way to do itlogoutSuccessHandler
: to run extra logic when logged out. this is basically a way to hook into the logout process and run some custom logic. So for example, when you have other external systems that need to be aware when you're logging out
- Helper, artificial, concept in spring that is helpful in some scenarios
- There are scenarios where if no principal is currently logged in, then a lot of extra code is needed (write) and a lot of extra logic to work around that problem
Scenario 1 Login
: common login config is including the username in the log message, in order to debug or trace activities by username. When that logging logic runs within a non-secured context (this login page) that logic will have to deal with anull
principal. Unless exists adefault
anonymous principal to put in the log message, the system is goint to have to deal with that null. So that anonymous authentication just helps in that scenario.Scenario 2 Auditing
: in most systems, audit logs will have a user, the problem is that when generating an audit entry from a nonsecured part of the application, we run into the same problem where don't have user to use in the audit entry. That is why this anonymous authentication or anonymous user can help. And again, once you're authenticated in the application, the real principal will be available in the Spring Security context, so this is just for those areas of the application, where you are not yet authenticated.
- Anonymous authentication token is going to be available whenever a real principal, an authenticated principal, is not available
- For example, if the audit code is using the principal out of the Spring Security authentication, there is no need to write special code, and there is no need to do null checking or any other checks on the authentication, and everything is going to be working out of the box
- Dependency for spring data and spring boot is easy
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
- For development and test is good to use in memory data bases like hsql
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
<!-- <dependency> -->
<!-- <groupId>mysql</groupId> -->
<!-- <artifactId>mysql-connector-java</artifactId> -->
<!-- <version>${mysql.version}</version> -->
<!-- </dependency> -->
- Configuration is done with java annotations
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@Configuration
@EnableJpaRepositories(basePackages = "com.maurofokker.demo.persistence")
@EntityScan("com.maurofokker.demo.web.model")
public class DemoPersistenceJpaConfig {
}
- Entities are annotated
import org.hibernate.validator.constraints.NotEmpty;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.Calendar;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotEmpty(message = "Username is required.")
private String username;
@NotEmpty(message = "Email is required.")
private String email;
private Calendar created = Calendar.getInstance();
// getters and setters
}
- For crud operations spring data comes with handy functions out of the box
import com.maurofokker.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
-
JpaRepository
andMongoRepository
interfaces extendCrudRepository
It takes the domain class to manage as well as the id type of the domain class as type arguments TheCrudRepository
provides sophisticated CRUD functionality for the entity class that is being managed -
CrudRepository
interface
public interface CrudRepository<T, ID extends Serializable>
extends Repository<T, ID> {
<S extends T> S save(S entity);
T findOne(ID primaryKey);
Iterable<T> findAll();
Long count();
void delete(T entity);
boolean exists(ID primaryKey);
// … more functionality omitted.
}
- Controller method to display registration form
@RequestMapping(value = "signup")
public ModelAndView registrationForm() {
return new ModelAndView("registrationPage", "user", new User());
}
- Thymeleaf registration page
- Controller method to registration logic from registration form action
@RequestMapping(value = "user/register")
public ModelAndView registerUser(@Valid User user, BindingResult result) {
if (result.hasErrors()) {
return new ModelAndView("registrationPage", "user", user);
}
try {
userService.registerNewUser(user);
} catch (EmailExistsException e) {
result.addError(new FieldError("user", "email", e.getMessage()));
return new ModelAndView("registrationPage", "user", user);
}
return new ModelAndView("redirect:/login");
}
- Service method to implement registration of new user logic
@Override
public User registerNewUser(final User user) throws EmailExistsException {
if (emailExist(user.getEmail())) {
throw new EmailExistsException("There is an account with that email address: " + user.getEmail());
}
return repository.save(user);
}
private boolean emailExist(String email) {
final User user = repository.findByEmail(email);
return user != null;
}
- Security config to allow access to registration form
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/signup", "/user/register").permitAll() // give access to url and operation
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll() // login form page, exception to be available for people not logged in
.loginProcessingUrl("/doLogin") // login proccesion url where authentication happens
.and()
.logout()
.permitAll().logoutUrl("/logout")
.and()
.csrf().disable()
;
}
- Authentication with newly registered users that were persisted in db
- Implementation of spring security UserDetailsService interface
@Transactional
@Service
public class DemoUserDetailsService implements UserDetailsService {
// needed bc there are gonna be persistence work
// to retrieve user
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(final String email) throws UsernameNotFoundException {
final User user = userRepository.findByEmail(email);
if (user == null) {
throw new UsernameNotFoundException("No user found with email: " + email);
}
return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), true, true, true, true, getAuthorities("ROLE_USER"));
}
/**
* wrapping authorities in the format spring security expects
* add authority in collection
* @param role
* @return
*/
private Collection<? extends GrantedAuthority> getAuthorities(String role) {
return Arrays.asList(new SimpleGrantedAuthority(role));
}
}
- Wire UserDetailsService in security configuration
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
- Activate registration using a verification token
@Entity
public class VerificationToken {
private static final int EXPIRATION = 60 * 24;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String token;
@OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
@JoinColumn(nullable = false, name = "user_id")
private User user;
private Date expiryDate;
// getters and setters
}
- Persistence API for verification token
public interface VerificationTokenRepository extends JpaRepository<VerificationToken, Long> {
VerificationToken findByToken(String token);
}
- User is
disabled
by default when created - When created user is loaded and wired it with spring security user details service, account status (
enable
) is get from the user entity
@Override
public UserDetails loadUserByUsername(final String email) throws UsernameNotFoundException {
final User user = userRepository.findByEmail(email);
if (user == null) {
throw new UsernameNotFoundException("No user found with email: " + email);
}
return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), user.getEnabled(), true, true, true, getAuthorities("ROLE_USER"));
}
- During registration controller is sent an event to notify the newly created user (
RegistrationContoller.registerUser
) - Event is received by a listener that will send a verification email to new user to confirm registration (
RegistrationListener
)- Token is created
- Email is sent
- Confirm registration is received by
/registrationConfirm
API (RegistrationController.confirmRegistration
)- User is retrieved using token (loaded from db)
- Do some validations related to token dates
- Set user enabled in db
- Redirect to login page
- Add link to forgot password page
- Add form to trigger reset password by email to
/user/resetPassword
API - Add view controller to accesss to
forgotPassword
registry.addViewController("/forgotPassword").setViewName("forgotPassword");
- Add urls
/forgotPassword
and/user/resetPassword*
to the allowed list
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/signup"
, "/user/register"
, "/registrationConfirm*"
, "badUser*"
, "/forgotPassword*"
, "/user/resetPassword*"
).permitAll() // give access to url and operation
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll() // login form page, exception to be available for people not logged in
.loginProcessingUrl("/doLogin") // login proccesion url where authentication happens
.and()
.logout()
.permitAll().logoutUrl("/logout")
.and()
.csrf().disable()
;
}
- Implementation of reset password logic
- Controller receive reset password request
- Load user by email
- If user exists, create password reset token for user (this is different token from creation because manage expiration)
- Token is send to user via Email just like confirmation
- TODO: this step could be managed by event and listener
@RequestMapping(value = "/user/resetPassword", method = RequestMethod.POST)
@ResponseBody
public ModelAndView resetPassword(final HttpServletRequest request, @RequestParam("email") final String userEmail, final RedirectAttributes redirectAttributes) {
final User user = userService.findUserByEmail(userEmail);
if (user != null) {
final String token = UUID.randomUUID().toString();
userService.createPasswordResetTokenForUser(user, token);
final String appUrl = "http://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath();
final SimpleMailMessage email = constructResetTokenEmail(appUrl, token, user);
mailSender.send(email);
}
redirectAttributes.addFlashAttribute("message", "You should receive an Password Reset Email shortly");
return new ModelAndView("redirect:/login");
}
- Service method for token reset creation
@Override
public void createPasswordResetTokenForUser(final User user, final String token) {
final PasswordResetToken myToken = new PasswordResetToken(token, user);
passwordTokenRepository.save(myToken);
}
PasswordResetToken
entity to control lifetime expiration of token
@Entity
public class PasswordResetToken {
private static final int EXPIRATION = 60 * 24;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String token;
@OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
@JoinColumn(nullable = false, name = "user_id")
private User user;
private Date expiryDate;
public PasswordResetToken() {
super();
}
public PasswordResetToken(final String token, final User user) {
super();
this.token = token;
this.user = user;
this.expiryDate = calculateExpiryDate(EXPIRATION);
}
// setter and getters
private Date calculateExpiryDate(final int expiryTimeInMinutes) {
final Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(new Date().getTime());
cal.add(Calendar.MINUTE, expiryTimeInMinutes);
return new Date(cal.getTime().getTime());
}
}
- Persistence repo for PasswordResetToken
public interface PasswordResetTokenRepository extends JpaRepository<PasswordResetToken, Long> {
PasswordResetToken findByToken(String token);
}
- Add controller method to show
reset password
page sent via email using the password reset token
@RequestMapping(value = "/user/changePassword", method = RequestMethod.GET)
public ModelAndView showChangePasswordPage(@RequestParam("id") final long id, @RequestParam("token") final String token, final RedirectAttributes redirectAttributes) {
final PasswordResetToken passToken = userService.getPasswordResetToken(token);
if (passToken == null) {
redirectAttributes.addFlashAttribute("errorMessage", "Invalid password reset token");
return new ModelAndView("redirect:/login");
}
// retrieve user with passToken
// check if password reset token is expired
// create nee authentication with UsernamePasswordAuthenticationToken
final Authentication auth = new UsernamePasswordAuthenticationToken(user, null, userDetailsService.loadUserByUsername(user.getEmail()).getAuthorities());
// set the principal auth for the context of the next operation where is going to be save in db
SecurityContextHolder.getContext().setAuthentication(auth);
// return to resetPassword page where user must enter new password
return new ModelAndView("resetPassword");
}
- Add
resetPassword.html
to reset password - Add controller method triggered when user send new password
@RequestMapping(value = "/user/savePassword", method = RequestMethod.POST)
@ResponseBody
public ModelAndView savePassword(@RequestParam("password") final String password, @RequestParam("passwordConfirmation") final String passwordConfirmation, final RedirectAttributes redirectAttributes) {
if (!password.equals(passwordConfirmation)) {
return new ModelAndView("resetPassword", ImmutableMap.of("errorMessage", "Passwords do not match"));
}
// principal authentication from security context
final User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
userService.changeUserPassword(user, password);
redirectAttributes.addFlashAttribute("message", "Password reset successfully");
return new ModelAndView("redirect:/login");
}
- Add urls
/user/changePassword
and/user/savePassword
to the allowed list
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/signup"
, "/user/register"
, "/registrationConfirm*"
, "badUser*"
, "/forgotPassword*"
, "/user/resetPassword*"
, "/user/changePassword*"
, "/user/savePassword*"
).permitAll() // give access to url and operation
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll() // login form page, exception to be available for people not logged in
.loginProcessingUrl("/doLogin") // login proccesion url where authentication happens
.and()
.logout()
.permitAll().logoutUrl("/logout")
.and()
.csrf().disable()
;
}
- Define security questions
- Security questions definition persistence with relation to user
- Add security questions to registration form
- Add security question to resgistration controller logic
- Use security question validation when reset password
- Add entity with security question definitions
@Entity
public class SecurityQuestionDefinition {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
@NotEmpty
private String text;
// setter getters
}
- Add entity with security questions relation with user and definitions
@Entity
public class SecurityQuestion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// relation with User
@OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
@JoinColumn(nullable = false, name = "user_id", unique = true)
private User user;
// relation with Security Question Definition
@OneToOne(targetEntity = SecurityQuestionDefinition.class, fetch = FetchType.EAGER)
@JoinColumn(nullable = false, name = "securityQuestionDefinition_id")
private SecurityQuestionDefinition questionDefinition;
private String answer;
public SecurityQuestion(final User user, final SecurityQuestionDefinition questionDefinition, final String answer) {
this.user = user;
this.questionDefinition = questionDefinition;
this.answer = answer;
}
// setter getters
}
- Repositories persistence for
SecurityQuestionDefinition
andSecurityQuestion
public interface SecurityQuestionDefinitionRepository extends JpaRepository<SecurityQuestionDefinition, Long> {
}
public interface SecurityQuestionRepository extends JpaRepository<SecurityQuestion, Long> {
// retrieve security question by question definition, user id and answer
SecurityQuestion findByQuestionDefinitionIdAndUserIdAndAnswer(Long questionDefinitionId, Long userId, String answer);
}
- Registration logic with questions
@RequestMapping(value = "signup")
public ModelAndView registrationForm() {
Map<String, Object> model = new HashMap<>();
model.put("user", new User());
model.put("questions", securityQuestionDefinitionRepository.findAll());
return new ModelAndView("registrationPage", model);
}
- Front will display questions
<div class="form-group">
<label class="control-label col-xs-2" for="question">Security Question:</label>
<div class="col-xs-10">
<select id="question" name="questionId">
<option th:each="question : ${questions}"
th:value="${question.id}"
th:text="${question.text}">Question</option>
</select>
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-2" for="answer">Answer</label>
<div class="col-xs-10">
<input id="answer" type="text" name="answer"/>
</div>
</div>
- After persist user we need to persist question related to user. This should be in a single transaction (user creation and question persistence)
final SecurityQuestionDefinition questionDefinition = securityQuestionDefinitionRepository.findOne(questionId);
securityQuestionRepository.save(new SecurityQuestion(user, questionDefinition, answer));
- Secure password reset with security question related
if (securityQuestionRepository.findByQuestionDefinitionIdAndUserIdAndAnswer(questionId, user.getId(), answer) == null) {
final Map<String, Object> model = new HashMap<>();
model.put("errorMessage", "Answer to security question is incorrect");
model.put("questions", securityQuestionDefinitionRepository.findAll());
return new ModelAndView("resetPassword", model);
}
- Should be done in both, frontend and backend
- Should give immediate feedback to user about strength of the password
- This will help the user to know if psw is secure in real time with feddback and save the hit to the backend for validation
- Ensure resolution mechanism for static resources are able
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(new String[] { "classpath:/static/" });
}
- Use the jquery (in this case) plugin jQuery Password Strength Meter for Twitter Bootstrap
<script src="/js/jquery-1.7.2.js"></script>
<script src="/js/pwstrength.js"></script>
- Use of jquery plugin to attach password strength mechanism to password field in form
<script type="text/javascript">
$(document).ready(function () {
options = {
common: {minChar:8},
ui: {
showVerdictsInsideProgressBar:true,
showErrors:true,
errorMessages:{
wordLength: 'Your password is too short',
}
}
};
$('#password').pwstrength(options);
});
</script>
- rule defined for psw strength is
common: {minChar:8}
there are more options
- It is good to verify password strength rules in the backend too
- Dependency for password validation library
<!-- Password Validation -->
<dependency>
<groupId>org.passay</groupId>
<artifactId>passay</artifactId>
<version>1.0</version>
</dependency>
- Good way is define a custom validator for the password and add logic in that validator. And this is going to be annotated in password field of entity
/**
* This is the annotation (will go on password field of entity)
*/
@Documented
@Constraint(validatedBy = PasswordConstraintValidator.class)
@Target({ TYPE, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface ValidPassword {
String message() default "Invalid Password";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
/**
* This is the logic of validation using passay (logic of annotation)
*/
public class PasswordConstraintValidator implements ConstraintValidator<ValidPassword, String> {
@Override
public void initialize(final ValidPassword arg0) {
}
@Override
public boolean isValid(final String password, final ConstraintValidatorContext context) {
// length rule btw 8 and 30 chars, ...
final PasswordValidator validator = new PasswordValidator(Arrays.asList(new LengthRule(8, 30), new UppercaseCharacterRule(1), new DigitCharacterRule(1), new SpecialCharacterRule(1), new WhitespaceRule()));
final RuleResult result = validator.validate(new PasswordData(password));
if (result.isValid()) {
return true;
}
// if validation is false add information to validation context, so frontend can displey that
context.disableDefaultConstraintViolation();
// API to add custom message that represents a constraint violation... that information is in the result
context.buildConstraintViolationWithTemplate(Joiner.on("\n").join(validator.getMessages(result))).addConstraintViolation();
return false;
}
}
- Use annotation to validate password on user entity
@Entity
@PasswordMatches
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Email
@NotEmpty(message = "Username is required.")
private String email;
// use annotation to validate password
@ValidPassword
@NotEmpty(message = "Password is required.")
private String password;
@Transient
@NotEmpty(message = "Password confirmation is required.")
private String passwordConfirmation;
@Column
private Boolean enabled;
private Calendar created = Calendar.getInstance();
// setters getters
}
- Logical flow
- User request use
RememberMeAuthenticationFilter
- if
check cookie
is ok then go to next stepdecode cookie
else go to next filter - if
decode cookie
is ok then go to next stepvalidate cookie
else throw exception - if
validate cookie
is ok then go to next stepcheck user account
else throw exception - if
check user account
is ok thencreate authentication token
and go to next filter
- Backend configuration in
configure(HttpSecurity http)
method
.and().rememberMe()
- Frontend configuration is a checkbox in loginpage
<div class="form-group">
<label class="control-label col-xs-2" for="remember"> Remember Me? </label>
<div class="col-xs-10">
<input id="remember" type="checkbox" name="remember-me" value="true" />
</div>
</div>
- Considerations
- basic remember me adds a new cookie
remember
in the browser in addition toJSESSION
- if
JSESSION
cookie is removed in a no-remember-me session then user will be redirected to login page when page is reloaded - if
JSESSION
cookie is removed in a remember-me session then user will not be redirected to login page when page is reloaded - remember me cookie lives 2 weeks by default
- default cookie es
remember-me
and should go in name attribute of checkbox - other parameters are allowed to change default behavior
- basic remember me adds a new cookie
- Default mode of remember me option in spring security is by cookie
- Spring security cookie based configuration
base64(username + ":" + expirationTime + ":" +
md5Hex(username + ":" + expirationTime + ":" password + ":" + key))
username: As identifiable to the UserDetailsService
password: That matches the one in the retrieved UserDetails
expirationTime: The date and time when the remember-me token expires, expressed in milliseconds
key: A private key to prevent modification of the remember-me token
- Some othe parameters are
.rememberMe().tokenValiditySeconds(604800).key("demosecapp").rememberMeCookieName("sticky-cookie").rememberMeParameter("remember")
.tokenValiditySeconds(604800)
: allow to change expiration date, default is 2 weeks and we can set one week instead.key("demosecapp")
: secret value that the system use to identify the tokens generated by our application, framework uses this secret value if tokens are valid.useSecureCookie(true)
: secure the cookie so the cookie is no longer being sent for unsecured connections. In local development is better not to use it because HTTPS. The cookie will existing but will simply be ignored and have no effect..rememberMeCookieName("sticky-cookie")
: change the name of the cookie, from the default value of remember-me to any other, the reaon to change the name is to not expose any of the underlying details of the framework we are using to secure our application..rememberMeParameter("remember")
: change default value remember-me for the same reason above
- This is more secure than cookie remember-me because only the
username
is present in the cookie, in other case thepassword
is used too - If something bad happen and cookie is compromised, just delete token in db
- In security config should wire up
DataSource
bean - Persistence token is done using the
JdbcTokenRepositoryImpl
ofPersistentTokenRepository
and settingdatasource
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
- Remember me persistence is done by adding
.tokenRepository(persistentTokenRepository())
inconfigure(HttpSecurity http)
method
.rememberMe()
.key("demosecapp")
.tokenValiditySeconds(604800) // 1 week = 604800
.tokenRepository(persistentTokenRepository())
.rememberMeParameter("remember")
- Table structure for persistence should be like this (according to documentation)
create table persistent_logins (username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null)
- concern in protection
- Less secure
- Is deprecated
- Java configuration in
Security Config
bean
@Bean
public PasswordEncoder passwordEncoder() {
return new Md5PasswordEncoder(); // deprecated MD% password encoder implementation
}
- Use in password setting results in MD5
5f4dcc3b5aa765d61d8327deb882cf99
user.setPassword(passwordEncoder().encodePassword("password", null));
- Security configuration to use password encoder
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
- More secure because use sha-256
- Is the standard option
- Java configuration in
Security Config
bean
@Bean
public PasswordEncoder passwordEncoder() {
return new StandardPasswordEncoder(); // this is the standard enconder sha-256
}
- Use in password setting results in sha-256
5a1ddadef8ea0bfc78ad8572ffe282e2f452f847eb870ae92b4ae79888f014ea253377bfa8c51ab9
user.setPassword(passwordEncoder().encode("password")); // stardard encoder sha-256
- User service that save password shoul wire up
PasswordEncoder
and encode password
-
SALT can be saved in db, dont need to be hidden
-
SALT should be unique per credential
-
SALT should be fixed length
-
SALT should be cryptographically strong random value
-
Spring security
StandardPasswordEncoder
implementation uses a SALT by default that is secureclass SecureRandomBytesKeyGenerator implements BytesKeyGenerator
- This SALT implementation meet above conditions
- Benefits
- Uses built-in salt value, different for each psw
- Random is a 16 byte value (for salt)
- Support for key stretching with a slow algorithm
- Amount of work for key stretching can be set with
strength
parameter wich takes values from 3 to 31 and default value is 10. - The higher the strength value, more work has to be done to calculate the hash
- It is important to know that strength value can be change without affecting existing passwords, because the value is stored in the encoded hash (see below)
- Bcrypt with strength 12
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // implements bcrypt encoder
}
- Bcrypt encoding for
password
gives (in this case)$2a$12$43gaubWA1jlYdi.JOxwGAe/BNopGQbC5ThRws2Gj6W74Mr/fMlhn.
part | description |
---|---|
indicates bcrypt hash | |
12$ | strength |
43gaubWA1jlYdi.JOxwGAe | 22 characters salt |
/BNopGQbC5ThRws2Gj6W74Mr/fMlhn. | 31 characters hash value |
- Allow you to run some operations under different principal with different authorities without logout and login with different user
- Some scenarios of use
- System that need to call remote services
- The need of a temporal privileges elevation of the current logged user (generating a new report that needs to access more data than the user may regularly need to see)
- Configure method security with RunAsManager bean
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class DemoMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected RunAsManager runAsManager() {
final RunAsManagerImpl runAsManager = new RunAsManagerImpl();
runAsManager.setKey("MyRunAsKey");
return runAsManager;
}
}
- Set up new authentication provider for RunAs (note that the key must be the same)
@Bean
public AuthenticationProvider runAsAuthenticationProvider() {
final RunAsImplAuthenticationProvider authProvider = new RunAsImplAuthenticationProvider();
authProvider.setKey("MyRunAsKey"); // same as DemoMethodSecurityConfig.runAsManager method
return authProvider;
}
- Should be wired in
AuthenticationManagerBuilder
ofconfigureGlobal(AuthenticationManagerBuilder auth)
method
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider());
auth.authenticationProvider(runAsAuthenticationProvider());
}
- Because the use of an additional authentication provider
userDetailsService
is going to managed from another authentication provider
@Bean
public AuthenticationProvider daoAuthenticationProvider() {
final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
-
Create a Controller with extra role
@Secured({ "ROLE_USER", "RUN_AS_REPORTER" })
-
Create a Service with method secured with
@Secured({ "ROLE_RUN_AS_REPORTER" })
-
Note that the
RUN_AS_REPORTER
at the Controller level is just a marker role and not an actual role assigned to the user -
This previous
RUN_AS*
marker is converted to the new authority, receives the extraROLE_
prefix in the process, and is now available on the current Authentication object -
Finally add new
DemoMethodSecurityConfig.class
to theSpringSecurityDemoApplication.class
- Default filter chain list
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
LogoutFilter
RequestCacheAwareFilter
SecurityContextHoldeAwareRequestFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
- New custom filter must extend
GenericFilterBean
and overridedoFilter
method
@Component
public class LoggingFilter extends GenericFilterBean {
private final Logger log = Logger.getLogger(LoggingFilter.class);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// some filter logic
filterChain.doFilter(servletRequest, servletResponse); // implementation
}
}
- To add new custom filter to security config
- Wire up filter
@Autowired
private LoggingFilter loggingFilter;
- Set in filter chain (before or after another filter, or let spring set position)
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(loggingFilter, AnonymousAuthenticationFilter.class) // add custom LoggingFilter in chain before of AnonymousAuthenticationFilter
.authorizeRequests()
// more configuration
.csrf().disable()
;
}
- After configuration filter chain is (custom filter is set before)
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
LogoutFilter
RequestCacheAwareFilter
SecurityContextHoldeAwareRequestFilter
RememberMeAuthenticationFilter
LoggingFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
- Should implement
AuthenticationProvider
interface if we call to a third party system - Contract in
authenticate
method of interface- if authentication succeeds, a full Authentication object (with credentials) is expected as the return
- if the provider doesn’t support the Authentication input, it will return null (and the next provider will be tried)
- if the provider does support it and we attempt authentication and fail - AuthenticationException
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
final String name = authentication.getName();
final String password = authentication.getCredentials().toString();
if (!supportsAuthentication(authentication)) { // check if this implementation manage authentication
return null;
}
/**
* Check authentication in 3rd party system, if its ok then return credentials in this case
* if 3rd party system fails then must manage data to send exception. 3rd party system could send more
* data than simple true or false and this system needs to manage that information in case of throw exception
* Is better to control all exceptions that you can that are extends from AuthenticationException
*/
if (doAuthenticationAgainstThirdPartySystem()) { // could do authentication in 3rd party system but in this case just return credentials
return new UsernamePasswordAuthenticationToken(name, password, new ArrayList<>());
} else {
throw new BadCredentialsException("Authentication against the third party system failed");
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
//
private boolean doAuthenticationAgainstThirdPartySystem() {
return true;
}
private boolean supportsAuthentication(Authentication authentication) {
return true; // becausa this provider will manage authentication
}
}
- Also could extend
AbstractUserDetailsAuthenticationProvider
likeDaoAuthenticationProvider
because handle things likeencoder
andsalt
- In case an Authentication provider is not handle authentication then let other provider to do it
- Override default provider
auth.userDetailsService(userDetailsService)
inconfigureGlobal(AuthenticationManagerBuilder auth)
method
@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); // this contains by default DaoAuthenticationProvider
auth.authenticationProvider(customAuthenticationProvider); // should implement encoder and salt but is for simple login
}
ProviderManager
shows providers list registered in case you want to see if custom provider is being use
- This could be done in the
configureGlobal(AuthenticationManagerBuilder auth)
method... i.e 3 providers
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider);
auth.authenticationProvider(daoAuthenticationProvider());
auth.authenticationProvider(runAsAuthenticationProvider());
}
- Define an auth manager bean is done in rare advanced cases
- If an auth manager can not authenticate then it goes to the parent
- This let you the possibility to change the hierarchy and plug in a sort of fallback auth manager in case all others fall
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.parentAuthenticationManager(new ProviderManager(Lists.newArrayList(customAuthenticationProvider)));
}
- Also is possible to confgure the auth manager
- By default, the ProviderManager will clear sensitive credentials information from the Authentication object which is returned by a successful authentication request.
- This prevents information like passwords being retained longer than necessary.
- In some rare cases, system need to change that - for example, say we’re storing these authentication objects into a cache (maybe they’re expensive to get back).
- To disable the clearing of credentials can set next configuration
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
ProviderManager authenticationManager = new ProviderManager(Lists.newArrayList(customAuthenticationProvider));
authenticationManager.setEraseCredentialsAfterAuthentication(false);
auth.parentAuthenticationManager(authenticationManager);
}
- Is important to keep in mind that the config API allows above configuration without messing with the actual manager bean
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.eraseCredentials(false).userDetailsService(userDetailsService);
}
- Authentication done with in memory user storage
@EnableWebSecurity
public class BasicSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user1").password("password1").roles("USER")
.and()
.withUser("user2").password("password2").roles("ADMIN")
;
}
}
- Authentication done with jdbc user storage
@Autowired private DataSource dataSource;
@EnableWebSecurity
public class BasicSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource).withDefaultSchema()
.withUser("user1").password("password1").roles("USER")
.and()
.withUser("user2").password("password2").roles("ADMIN")
;
}
}
withDefaultSchema()
just work for h2 db, in mysql this wont work because it doesnt have typevarchar_ignorecase
- Structure for MySQL
create schema if not exists ssdemo;
USE ssdemo;
create table users(
username varchar(50) not null primary key,
password varchar(500) not null,
enabled boolean not null
);
create table authorities (
username varchar(50) not null,
authority varchar(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);
- With above schema
withDefaultSchema()
configuration is not longer needed
auth.jdbcAuthentication().dataSource(dataSource)
- For others non standard db structure (own structure) the configuration allows use to set up
.usersByUsernameQuery( ... )
.authoritiesByUsernameQuery( ... )
- This is shown in the code and describe it earlier in this documentation
- SecurityContextHolder is the storage mechanism for the security information associated to the running thread, it uses a ThreadLocal to store de user details which hold a single context per thread, in an Async call that context is lost
- Strategy to propagate security context to new threads:
- Pass as environment property as VM Option parameter at startup:
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
- Add to application.properties:
spring.security.strategy=MODE_INHERITABLETHREADLOCAL
- Add programatically:
SecurityContextHolder.setStrategyName("MODE_INHERITABLETHREADLOCAL")
- Pass as environment property as VM Option parameter at startup:
- Test if current user pas in new thread
@Async
public void asyncCall() {
log.info("async call... {}", SecurityContextHolder.getContext().getAuthentication());
}
- Security context is mantained between requests (or user operations), in MVC app after login, the user is identified by its session id.
the management of the context is done by the
SecurityContextPersistenceFilter
. And by default, it stores the context as an attribute of the HTTP session, and it then restores it for each request and clears it when the request ends. - If the system is stateless (no session), like in a REST API,
SecurityContextPersistenceFilter
is still needed for this logic. - Store security context between requests
- Configuration goes when configuring
HttpSecurity
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// configurations
// ...
.and()
.sessionManagement()
.maximumSessions(1)
.sessionRegistry(sessionRegistry()) // register this session registry into our security configuration
.and()
.sessionFixation().none() // this is need to close the session configuration
.and()
// continues remember me configuration
// and csrf disabling
;
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
- Active users can be managed in a service that wire
SessionRegistry
@Service
public class ActiveUserService {
private static Logger log = LoggerFactory.getLogger(ActiveUserService.class);
@Autowired
private SessionRegistry sessionRegistry; // already wired in BasicSecurityConfig
public List<String> getAllActiveUsers() {
//List<User> principals = sessionRegistry.getAllPrincipals(); // return List<Object> (parent) and cannot be casted to List<User> (child)
List<Object> principals = sessionRegistry.getAllPrincipals();
User[] users = principals.toArray(new User[principals.size()]); // convert to array of Users
return Arrays.stream(users)
.filter(u -> !sessionRegistry.getAllSessions(u, false).isEmpty()) // get active sessions from all sessiones
.map(u -> u.getUsername()) // get usernames
.collect(Collectors.toList())
;
}
}
- Get active users to view can be done by call service created above
@Autowired private ActiveUserService activeUserService;
@RequestMapping
public ModelAndView list() {
asyncBean.asyncCall(); // call to asyng method to see what happen with spring security context
// return just active users
List<User> users = activeUserService.getAllActiveUsers().stream()
.map(s -> userRepository.findByEmail(s)).collect(Collectors.toList());
log.info("users -> {}", users);
return new ModelAndView("users/list", "users", users);
}
- Web Requests (url requuest) is secured with the
FilterSecurityInterceptor
object - Method Invocations (method in a class -controller-) objects are secured with the
MethodSecurityInterceptor
object - Implementations mentioned above extends
AbstractSecurityInterceptor
that has the most part of the interceptor logic - Main driver in th interceptor logic runs in the
before invocaion
flow that runs before any invocation (web or method)- Get the configuration attributes for the particular request
- Attempt to authoriza delegating to
AccessDecisionManager
2.1. If access isgranted
then the invocation proceeds 2.2. If access isnot granted
then throw anAccessDeniedException
- Better order and clean keeping security configurations centralized in just one place
- When is not possible annotate sorcer code because there is no access to it and then it cannot be modify it
- Method secured are authorized twice, one by the url (web request) and secondly authorize with the method invocation
- Implementation to secure method invocation using AOP
@EnableGlobalMethodSecurity(prePostEnabled = true)
public static class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
public MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
final Map<String, List<ConfigAttribute>> methodMap = new HashMap<>(); // map with methods to secure
methodMap.put("com.maurofokker.demo.web.controller.UserController.createForm*", SecurityConfig.createList("ROLE_ADMIN"));
return new MapBasedMethodSecurityMetadataSource(methodMap);
}
}
- Nota: in order to use role authorities (instead of privileges)
DemoUserDetailsService.getAuthorities
should be implemented
public final Collection<? extends GrantedAuthority> getAuthorities(final Collection<Role> roles) {
return roles.stream()
//.flatMap(role -> role.getPrivileges().stream()) // this go deep to privilage level authorities
.map(p -> new SimpleGrantedAuthority(p.getName())) // commented above line this get first level role authorities
.collect(Collectors.toList());
}
- To debug above a break point can be placed in
AbstractSecurityInterceptor.beforeInvocation(..)
and see what implementation is used
- Starter in the authorization flow
- It implementations
AffirmativeBased
: any affirmative vote will grant accessConsensusBased
: need a majority of affirmative vote to grant accessUnanimousBased
: all affirmative vote are required to grant access (abstains doesn't count)
- A Voter is a rule that can grant or restrict access to a resource
- Base implementation is
AccessDecisionVoter
allow to implement own logic, but there are a lot of them in the framework - Some voter implementations
Role
: voter used directly when using Role based authorization- Exist one
Hierarchical Role
Authenticated
: voter that check for authentication when usingisAuthenticated()
expressionWeb expressions
:ACL
:
- Change from
AffirmativeBased
(default) to a more restrictiveUnanimousBased
(where custom voter will be added)
@Bean
public AccessDecisionManager unnanimous(){
List<AccessDecisionVoter<? extends Object>> voters = Lists.newArrayList(
new RoleVoter(), new AuthenticatedVoter(), new WebExpressionVoter());
return new UnanimousBased(voters);
}
- Must be wire up to the authotize request
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/secured").access("hasRole('ADMIN')") // web expression voter secured for specific role
.antMatchers("/signup"
, "/user/register"
, "/registrationConfirm*"
, "badUser*"
, "/forgotPassword*"
, "/user/resetPassword*"
, "/user/changePassword*"
, "/user/savePassword*"
, "/js/**"
).permitAll()
.anyRequest().authenticated()
.accessDecisionManager(unnanimous()) // use bean with unnanimous decision manager that add custom voter
.and()
// configurations..
.and()
.csrf().disable()
;
}
- To create the new voter this need to implement
AccessDecisionVoter<Object>
interface
public class RealTimeLockVoter implements AccessDecisionVoter<Object> { }
- Implementation of this in classes
RealTimeLockVoter
(custom voter) andLockedUsers
(simple cache for users locked)
- By default
AccessDecisionManager
isAffirmariveBased
- Works with a
RoleVoter
and anAuthenticatedVoter
- Above can be changed with configurations and set other voters
- Setup is done to allows everything at the URL and uses method security level (in this case to secure a form)
FilterSecurityInterceptor
is the last in the filter chain and is the entry point to call the AccessDecisionManager in the beforeInvocation call- This simply goes over the voters and, if any of the voters deny access, then the AccessDeniedException is thrown. Otherwise, access is granted.
- Authorization flow run twitce, first for the url and second for a method secured
- Flat topology for authorization means that one or more roles can be assigned to an expression
- Above is not very useful because for every new role that need to access to expression there must compile again the code
- It is better to have a two-level hierarchy topology than a flat one because this contain roles and privileges that can be added to role at runtime
- Main concepts
Privilege
: granular and low-level capability in systemRole
: high-level and user facingAuthority
:GrantedAuthority
class in spring; apply same concept asPrivilege
Permission
: apply same concept asPrivilege
Right
: apply same concept asPrivilege
GranthedAuthority
will be map to aPrivilege
and aRole
is a collection of privileges
- Privileges examples
Can see a ...
Can update ...
Can delete ...
Can see all of ...
- Roles examples
ROLE_ADMIN
->{READ_PRIVILEGE, WRITE_PRIVILEGE}
ROLE_USER
->{READ_PRIVILEGE}
- Relationship
User
andRole
is many to manyRole
andPrivilege
is many to many
- Add
Role
andPrivilege
entities
@Entity
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "roles_privileges", joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "privilege_id", referencedColumnName = "id"))
private Collection<Privilege> privileges;
private String name;
public Role() {
super();
}
public Role(final String name) {
super();
this.name = name;
}
// setter getter
}
@Entity
public class Privilege {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
@ManyToMany(mappedBy = "privileges")
private Collection<Role> roles;
public Privilege() {
super();
}
public Privilege(final String name) {
super();
this.name = name;
}
// setters and getters
}
- Add roles to
User
entity
@Entity
@PasswordMatches
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Email
@NotEmpty(message = "Username is required.")
private String email;
// use annotation to validate passw
@ValidPassword
@NotEmpty(message = "Password is required.")
private String password;
@Transient
@NotEmpty(message = "Password confirmation is required.")
private String passwordConfirmation;
@Column
private Boolean enabled;
private Calendar created = Calendar.getInstance();
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "users_roles", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private Collection<Role> roles;
// setters and getters
}
- Modify
UserDetailsService
to get authorities from user roles
@Transactional
@Service
public class DemoUserDetailsService implements UserDetailsService {
private static Logger log = LoggerFactory.getLogger(DemoUserDetailsService.class);
// needed bc there are gonna be persistence work
// to retrieve user
@Autowired
private UserRepository userRepository;
public DemoUserDetailsService() {
super();
}
@Override
public UserDetails loadUserByUsername(final String email) throws UsernameNotFoundException {
log.info("sercha username: {}", email);
final User user = userRepository.findByEmail(email);
log.info("user -> {}",user.toString());
if (user == null) {
throw new UsernameNotFoundException("No user found with email: " + email);
}
//todo: put enabled as user.getEnabled() after finish feature, by the moment im disabling account validation
return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), true, true, true, true, getAuthorities(user.getRoles()));
}
/**
* wrapping authorities in the format spring security expects
* add authority in collection
* @param roles
* @return
*/
public final Collection<? extends GrantedAuthority> getAuthorities(final Collection<Role> roles) {
return roles.stream()
.flatMap(role -> role.getPrivileges().stream())
.map(p -> new SimpleGrantedAuthority(p.getName()))
.collect(Collectors.toList());
}
}
- Create repositories for
Role
andPrivilege
- Basic Authentication -> stateless
- Digest Authentication -> stateless
- Form Based Authentication -> stateful
- OAuth2 -> stateful
- OAuth2 + JWT -> stateless
- Custom Token Authentication -> stateful or stateless
-
SAML (security assertion markup language)
- XML based
- Encriptions and signing options
- Expresive but need advanced XML Stack
-
Simple Web Token
- Much simpler than SAML
- Symetric, not enough cryptographic options
-
JWT
- Representing token using JSON (widely supported)
- Symmetric and asymmetric signatures and encryption
- Less flexibility than SAML but more than SWT
- Widely adopted
- Emerging protocol but very close to standardization
- Structure
header.payload.signature
Header
: declare the type and the hashing algorithmPayload
: contains the claims (information we want to transmit with token) that are registered, public and privateSignature
: made up of a hash of the header, payload and secret
- It uses the
Authorization
header to transmit base 64 encoded credentials over the wire - Problems
- Base 64 encoded credentials can easily converted to plain text
- It needs to run over HTTPS but SSL only protects the data over the wire; once the data hits the server, credentials can be potentially exposed (there may still be internal routing, logging, etc).
- Password is sent repeatedly, for each request
- Client get the full master key - the actual credentials, so if these get compromise, the impact will be significantly greater than with a token based solution
- Password is cached by the browser and is automatically sent to the server on new requests
- Opens up the system to potential CSRF vulnerabilities without extra protection
- Only covers authentication. You can’t tell what permissions the user has because it has no concept of and no semantics for authorization.
- There's no distinction being made between actual users and machines
- OAuth2 is a standard for authorization, more specifically, delegated authorization.
Resource Owner
(the user) is capable of granting access to a Resource.Resource Server
(the API) is the host of the protected Resources.Authorization Server
is capable of issuing Access Tokens to the Client.Client
(the front end app) is capable of making requests on behalf of the Resource Owner.
Confidential Clients
are capable of maintaining the confidentiality of their credentials (a server side client running on a secure server).Public Clients
cannot guarantee they're going to be able to maintain the confidentiality of their credentials (a native Android application, or an AngularJS client).
- The Client asks for access from the Resource Owner (User) - which grants it
- The Client talks to the Authorization Server
- The Authorization Server gives the Client a key (access token)
- With the Key, the Client talks to the Resource Server (API) and can access the Resource(s)
-
Authorization Code Flow
- used for server rendered applications- Client request authorization from the User
- Request token from the authorization server
- With the token then Access resource
-
Implicit Flow
- used forapplications that run on the user's device- Access token is retrieve in the authorization request (it doesn't need a separate request)
- With token can start access resource
- Doesn't have refresh token, once the token expires it need to go with the authorization process once again
- Is a redirection based flow, the client must be redirection capable
-
Client Credentials Flow
- used for Client to Server communication, as there is no Resource Owner involved at all.- Optimized for confidential clients and request access token with client credentials (client id and secret)
- With token can start access resources
- No resource owner is involved, so no user credentials are used
-
Resource Owner Password Credentials Flow
- used for trusted applications (such as those owned by the service itself, server to server app).- Request to the token endpoint using credentials (master key) and Authorization Server returns the access token
- With token can start access resources
- Users control some objects (
possessions
). And each user has the right to work only with his own possession objects - Secondly another user may need to use a possesion from another user (
borrow
) - In above case, the
owner
will grant some privileges to theborrower
over that particular possession - Authorization model used until now is not flexible enough to handle above scenario because it define authorities per type of object.
So it can define that users that have
this authority
can dothis action
onALL objects
of thistype
. But can't define that users that havethis authority
can dothis action
onobject A
of thistype
, but not onobject B of the SAME type
. - Granular access can be done in a manually way combining per-type authorization and custom business logic with extra checks but is not a good way because is difficult
- More flexible and more granular mode is using Spring Security new model that allow us to define the security semantics of a specific domain object, not just a class/type of objects.
- This model it’s a generic way of defining authorization semantics for each domain entity using what is called an access control list - ACL. And it's going to allow us full granular control over exactly which users in the system can access exactly which objects.
Security Identity (SID)
: represent the principal that gets access to the domain object. The SID can also represent an authorityDomain Object
: is composed of two entitiesClass
: the java class of the entityObject Identity
: the main identifier of the entity tryig to secure
ACL Entry
: represents the actual permissions that the principal has on the domain objects. By default these are: read, write, create, delete, admin - and they’re represented with an integer bit mask32 bits mask
: 5 bits for above permissions and other and rest can be used for custom types
{
"SID" : "Mauro",
"Domain Object": {
"Class": "Possession",
"ObjectId": "Car"
},
"Entry": "W (own)"
}
{
"SID" : "John",
"Domain Object": {
"Class": "Possession",
"ObjectId": "Car"
},
"Entry": "R (borrow)"
}
- MySQL version schema (others acl schemas)
CREATE TABLE IF NOT EXISTS acl_sid (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
principal BOOLEAN NOT NULL,
sid VARCHAR(100) NOT NULL,
UNIQUE KEY unique_acl_sid (sid, principal)
) ENGINE=InnoDB;
--
CREATE TABLE IF NOT EXISTS acl_class (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
class VARCHAR(100) NOT NULL,
UNIQUE KEY uk_acl_class (class)
) ENGINE=InnoDB;
--
CREATE TABLE IF NOT EXISTS acl_object_identity (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
object_id_class BIGINT UNSIGNED NOT NULL,
object_id_identity BIGINT NOT NULL,
parent_object BIGINT UNSIGNED,
owner_sid BIGINT UNSIGNED,
entries_inheriting BOOLEAN NOT NULL,
UNIQUE KEY uk_acl_object_identity (object_id_class, object_id_identity),
CONSTRAINT fk_acl_object_identity_parent FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id),
CONSTRAINT fk_acl_object_identity_class FOREIGN KEY (object_id_class) REFERENCES acl_class (id),
CONSTRAINT fk_acl_object_identity_owner FOREIGN KEY (owner_sid) REFERENCES acl_sid (id)
) ENGINE=InnoDB;
--
CREATE TABLE IF NOT EXISTS acl_entry (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
acl_object_identity BIGINT UNSIGNED NOT NULL,
ace_order INTEGER NOT NULL,
sid BIGINT UNSIGNED NOT NULL,
mask INTEGER UNSIGNED NOT NULL,
granting BOOLEAN NOT NULL,
audit_success BOOLEAN NOT NULL,
audit_failure BOOLEAN NOT NULL,
UNIQUE KEY unique_acl_entry (acl_object_identity, ace_order),
CONSTRAINT fk_acl_entry_object FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity (id),
CONSTRAINT fk_acl_entry_acl FOREIGN KEY (sid) REFERENCES acl_sid (id)
) ENGINE=InnoDB;
- Example setup
- Domain objects entities
@Entity
@PasswordMatches
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Email
@NotEmpty(message = "Email is required.")
private String email;
@ValidPassword
@NotEmpty(message = "Password is required.")
private String password;
@Transient
@NotEmpty(message = "Password confirmation is required.")
private String passwordConfirmation;
public User() {
super();
}
// setters and getters
}
@Entity
public class Possession {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "owner_id", nullable = false)
private User owner;
//
public Possession() {
super();
}
public Possession(String name) {
super();
this.name = name;
}
// getters and setters
}
- Artifacts of configurations
INSERT INTO acl_sid (id, principal, sid)
VALUES
(1, 1, 'user1'),
(2, 1, 'user2');
--
INSERT INTO acl_class (id, class)
VALUES
(1, 'com.maurofokker.demo.model.Possession');
--
INSERT INTO acl_object_identity
(id, object_id_class, object_id_identity, parent_object, owner_sid, entries_inheriting)
VALUES
(1, 1, 1, NULL, 1, 1), -- user1 Possession object identity
(2, 1, 2, NULL, 1, 1), -- Common Possession object identity
(3, 1, 3, NULL, 1, 1); -- user2 Possession object identity
- Entry configurations
-- each has access to their own possessions, and shared posession is shared to both users to access
INSERT INTO acl_entry
(id, acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure)
VALUES
(1, 1, 0, 1, 16, 1, 0, 0), -- user1 has Admin permission for Possession 1
(2, 2, 0, 1, 16, 1, 0, 0), -- user1 has Admin permission for Common Possession 2
(3, 2, 1, 2, 1, 1, 0, 0), -- user2 has Read permission for Common Possession 2
(4, 3, 0, 2, 16, 1, 0, 0); -- user2 has Admin permission for Eric Possession 3