The following line doesn't let me visit GET: /api/topics without a bearer token. It works if I apply a token. Am I missing something? Isn't permitAll supposed to do that?
.antMatchers("/api/topics/**").permitAll()
By the way, I tried with /api/topics** and it didn't work as well.
Error:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
Result without a token (the broken part). I want it to let me through.
Result with a token. It works as intended:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(jsr250Enabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/topics/**").permitAll()
.antMatchers("/api/users/**").permitAll()
.anyRequest().authenticated();
}
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
#RestController
#RequestMapping("/api/topics")
public class TopicController {
#Autowired
private TopicService topicService;
#Autowired
private UserService userService;
#Autowired
private TopicMapper topicMapper;
/**
* Gets all topics.
*
* #return the topics.
*/
#GetMapping
public ResponseEntity<List<TopicDTO>> getAll() {
return ResponseEntity.ok(topicMapper.toTopicDTOs(topicService.getAll()));
}
/**
* Gets topic by id.
*
* #param id the id.
* #return the topic.
*/
#GetMapping("/{id}")
public ResponseEntity<TopicDTO> get(#PathVariable("id") Long id) {
Optional<TopicEntity> topicEntity = topicService.get(id);
return topicEntity.map(entity -> ResponseEntity.ok(topicMapper.toTopicDTO(entity))).orElseGet(() -> ResponseEntity.notFound().build());
}
/**
* Creates a new topic.
*
* #param topicDTO the topic DTO.
* #return the new topic DTO.
*/
#PostMapping
public ResponseEntity<TopicDTO> create(#RequestBody TopicDTO topicDTO) {
UserEntity userEntity = userService.get(topicDTO.getUserId()).orElseThrow(() -> new IllegalArgumentException("User does not exist."));
TopicEntity topicEntity = topicMapper.toTopicEntity(topicDTO);
topicEntity.setId(null);
topicEntity.setUser(userEntity);
Optional<TopicEntity> createdTopicEntity = topicService.create(topicEntity);
return createdTopicEntity.map(entity -> ResponseEntity.ok(topicMapper.toTopicDTO(entity))).orElseGet(() -> ResponseEntity.status(HttpStatus.CONFLICT).build());
}
/**
* Updates an existing topic.
* #param id the topic id.
* #param topicDTO the topic DTO.
* #return the updated topic DTO.
*/
#PutMapping("/{id}")
public ResponseEntity<TopicDTO> update(#PathVariable("id") Long id, #RequestBody TopicDTO topicDTO) {
UserEntity userEntity = userService.get(topicDTO.getUserId()).orElseThrow(() -> new IllegalArgumentException("User does not exist."));
TopicEntity topicEntity = topicMapper.toTopicEntity(topicDTO);
topicEntity.setId(id);
topicEntity.setUser(userEntity);
Optional<TopicEntity> updatedTopicEntity = topicService.update(topicEntity);
return updatedTopicEntity.map(entity -> ResponseEntity.ok(topicMapper.toTopicDTO(entity))).orElseGet(() -> ResponseEntity.badRequest().build());
}
/**
* Deletes an existing topic.
* #param id the topic id.
* #return the status code.
*/
#DeleteMapping("/{id}")
public ResponseEntity<Void> delete(#PathVariable("id") Long id) {
if (topicService.get(id).isPresent()) {
topicService.delete(id);
return ResponseEntity.ok().build();
}
return ResponseEntity.notFound().build();
}
}
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
#Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
#Autowired
private TokenStore tokenStore;
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("trusted")
.secret(bCryptPasswordEncoder.encode("secret"))
.authorizedGrantTypes("password", "get_token", "refresh_token")
.scopes("read", "write")
.autoApprove(true)
.accessTokenValiditySeconds(15 * 60)
.refreshTokenValiditySeconds(30 * 60);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.tokenStore(tokenStore);
}
#Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration {
}
Because #EnableResourceServer will add its filter chain at order=3 by default.As for WebSecurityConfigurerAdapter implementation, adds its own filterchain at the order=100, as a result, request first goes through filter chain of set by #EnableResourceServer where everything is protected unless you provide token and that's why you are getting that behavior. Try to add order bellow 3 like #Order(2) annotation to your WebSecurityConfigurerAdapter implementation.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(jsr250Enabled = true)
#Order(2) <<--- add this
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
...
}
For more information read: Changing the Filter Order
There is a mistake with URL patterns in antMatchers.
By default the pattern like /api/topics/** doesn't match /api/topics.
It matches only /api/topics/ and after slash can be zero or more symbols
For fixing this case can be several solutions:
Change the pattern in existing antMatchers to next /api/topics**
Use mvcMatchers instead of antMatchers. mvcMatchers("/api/topics").permitAll()
mvcMatchers - will use the same rules that Spring MVC uses for
matching. For example, often times a mapping of the path "/path" will
match on "/path", "/path/", "/path.html", etc.
More information about antMatchers can be found here
More information about mvcMatchers can be found here
Related
I recently asked a question very similar to this one but instead of 401 the error I was getting was 403 (Forbbiden), but I changed the entire code so I decided to post a new one specific to this code and this problem.
I'm trying to create an user logic to my project (for the first time ever) but it has been impossible to implement any kind of security measure. I've been stuck in this for days so if anyone knows where I'm wrong I'd be grateful!
this is my code:
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/users/create", "/users/create/**").permitAll()
.and()
.httpBasic();
}
}
#Data
#Component
public class CreateUserRoleDTO {
private Integer idUser;
private List<Integer> idsRoles;
public CreateUserRoleDTO() {
super();
}
public CreateUserRoleDTO(Integer idUser, List<Integer> idsRoles) {
super();
this.idUser = idUser;
this.idsRoles = idsRoles;
}
public Integer getIdUser() {
return idUser;
}
public void setIdUser(Integer idUser) {
this.idUser = idUser;
}
public List<Integer> getIdsRoles() {
return idsRoles;
}
public void setIdsRoles(List<Integer> idsRoles) {
this.idsRoles = idsRoles;
}
}
#Service
public class CreateRoleUserService {
#Autowired
private UserRepository repo;
#Autowired
private CreateUserRoleDTO createUserRoleDTO;
public Users execute(CreateUserRoleDTO createUserRoleDTO) {
Optional<Users> userExists=repo.findById(createUserRoleDTO.getIdUser());
List<Roles> roles=new ArrayList<>();
if (userExists.isEmpty()) {
throw new Error("User does not exist");
}
roles=createUserRoleDTO.getIdsRoles().stream().map(role -> {
return new Roles(role);
}).collect(Collectors.toList());
Users user=userExists.get();
user.setRole(roles);
repo.save(user);
return user;
}
#Entity
#Table(name="users_table")
public class Users implements Serializable{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
#Column(unique=true)
private String login;
#Column(unique=true)
private String email;
private String password;
#ManyToMany
private List<Roles> role;
}
(plus the getters and setters and constructors)
data.sql:
INSERT INTO `ROLES`(`ID`, `NAME`) VALUES(1, 'USER');
INSERT INTO `ROLES`(`ID`,`NAME`) VALUES(2, 'ADMIN');
-> the code runs fine, it even gives me the security password, the problem appears when I try to make any kind of requests.
The entire code if I've left anything out: https://github.com/vitoriaacarvalho/backend-challenge-very-useful-tools-to-remember-
An authentication configuration is missing in your SecurityConfig. For example, try adding the following to your configure method:
http.httpBasic();
Additionally, your security configuration is missing a default authorization rule, so authentication is not actually required. You can try adding .anyRequest().authenticated() to test this out.
Here's a configuration which uses the lambda syntax available in the DSL and is ready to be upgraded to Spring Security 6:
#Configuration
#EnableWebSecurity
#EnableMethodSecurity
public class SecurityConfig {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.antMatchers("/users/create", "/users/create/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
// Disable CSRF for testing.
// TODO: Delete the following line and learn about CSRF!
http.csrf().disable();
return http.build();
}
#Bean // Automatically injected into Spring Security
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// Note: We don't configure a UserDetailsService since it is already
// annotated #Service and therefore already published as an #Bean.
}
Unfortunately, I also spotted a few other mistakes in your application that made it not work.
It looks like you have a mistake in the JPQL used to query the user for the UserDetailsService. The WHERE clause should be where u.login = :username (add a u.).
You also have the if-statement inverted as well. When throwing a UsernameNotFoundException (a better exception than Error for this case), it would look like:
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users user = repo.findByUsernameFetchRoles(username);
if (user == null) {
throw new UsernameNotFoundException("User does not exist!");
}
return UserPrincipal.create(user);
}
Lastly, the constructor of your Users class was not assigning user data from the user parameter. It should be:
public UserPrincipal(Users user) {
this.login = user.getLogin();
this.password = user.getPassword();
...
}
With those changes, authentication works and you're on your way to learning Spring Security!
As mentioned in the title I can't update my webapp to Spring Boot 2.6.0. I wrote my webapp using Spring Boot 2.5.5 and everything works perfectly. If I update the pom.xml file with this new tag:
<version>2.5.7</version>
My webapp works perfectly. All tests work.
If I perform this update the webapp does not start:
<version>2.6.0</version>
Starting the DEBUG mode the IDE shows me an error and 2 links to 2 classes of my webapp.
2021-11-23 00:31:45.419 ERROR 21884 --- [ restartedMain] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'configurazioneSpringSecurity': Requested bean is currently in creation: Is there an unresolvable circular reference?
It seems the problem is in this class:
#Configuration
#EnableWebSecurity
public class ConfigurazioneSpringSecurity extends WebSecurityConfigurerAdapter {
#Autowired
LivelliDeiRuoli livelliDeiRuoli;
#Autowired
GestioneUtentiSpringSecurity gestioneUtentiSpringSecurity;
#Bean
public BCryptPasswordEncoder metodoCrittografia() {
return new BCryptPasswordEncoder();
}
#Autowired
public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(gestioneUtentiSpringSecurity).passwordEncoder(metodoCrittografia());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().antMatchers(
"/",
"/login",
"/benvenuto",
"/registrazione",
"/registrazione-eseguita",
"/pagine-applicazione"
).permitAll();
http.authorizeRequests().antMatchers("/area-riservata")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(1L) + "')");
http.authorizeRequests().antMatchers("/cambio-password")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(1L) + "')");
http.authorizeRequests().antMatchers("/cambio-nome")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(1L) + "')");
http.authorizeRequests().antMatchers("/cancella-utente")
.access("isAuthenticated()");
http.authorizeRequests().antMatchers("/gestione-utenti")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(2L) + "')");
http.authorizeRequests().antMatchers("/gestione-ruoli")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(3L) + "')");
http.authorizeRequests().antMatchers("/pannello-di-controllo")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(3L) + "')");
http.authorizeRequests().and().exceptionHandling().accessDeniedPage("/errore-403");
http.authorizeRequests().and().formLogin()
.loginProcessingUrl("/pagina-login")
.loginPage("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?errore=true")
.usernameParameter("username")
.passwordParameter("password")
.and().logout().logoutUrl("/pagina-logout")
.logoutSuccessUrl("/login?logout=true");
http.authorizeRequests().and() //
.rememberMe().tokenRepository(this.persistentTokenRepository()) //
.tokenValiditySeconds(365 * 24 * 60 * 60);
http.authorizeRequests().antMatchers("/gestione-eventi")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(2L) + "')");
http.authorizeRequests().antMatchers(
"/cerca-eventi",
"/ultimi-eventi"
).permitAll();
}
#Autowired
private DataSource dataSource;
#Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setDataSource(dataSource);
return db;
}
#Bean(name = BeanIds.AUTHENTICATION_MANAGER)
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
or in this:
#SpringBootApplication
#Profile("sviluppo")
public class GestioneUtentiApplication extends SpringBootServletInitializer {
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(GestioneUtentiApplication.class);
}
public static void main(String[] args) {
System.setProperty("server.servlet.context-path", "/gestioneutenti");
SpringApplication.run(GestioneUtentiApplication.class, args);
}
}
What's wrong with these classes?
What changes with Spring Boot 2.6.0?
GestioneUtentiSpringSecurity implements UserDetailsService:
#Service
public class GestioneUtentiSpringSecurity implements UserDetailsService {
#Autowired
private UtenteRepository utenteRepository;
#Autowired
private RuoloRepository ruoloRepository;
#Autowired
EseguiVariabiliDiSistema eseguiVariabiliDiSistema;
#Autowired
LivelliDeiRuoli livelliDeiRuoli;
#Override
public UserDetails loadUserByUsername(String nomeUtente) throws UsernameNotFoundException {
Utente utente = trovaUtenteConPrivilegiDiAutenticazione(nomeUtente);
if (utente == null) {
throw new UsernameNotFoundException("L'utente " + nomeUtente + " non è stato trovato nel database.");
}
List<String> ruoliUtente = null;
try {
ruoliUtente = this.ruoloRepository.trovaRuoliUtente(utente.getId());
}catch (Exception b){
ruoliUtente = null;
}
List<GrantedAuthority> grantList = null;
try{
grantList = new ArrayList<GrantedAuthority>();
if (ruoliUtente != null) {
for (String ruolo : ruoliUtente) {
GrantedAuthority authority = new SimpleGrantedAuthority(ruolo);
grantList.add(authority);
}
}
}catch (Exception c){
grantList = null;
}
UserDetails userDetails = null;
if((utente != null) && (ruoliUtente != null) && (grantList != null)){
userDetails = (UserDetails) new User(utente.getNome(), utente.getPassword(), grantList);
}
return userDetails;
}
public Utente trovaUtenteConPrivilegiDiAutenticazione(String nomeUtente){
try{
Utente utente = utenteRepository.trovaUtente(nomeUtente);
if(livelliDeiRuoli.requisitiUtenteConRuoloMassimo(utente)){
return utente;
} else{
eseguiVariabiliDiSistema.trovaVariabileSenzaVerificaUtente(
new VariabileSistema(0L, "login", "")
);
if(eseguiVariabiliDiSistema.getVariabileDiSistema().getValore().equals("true")){
return utente;
}else if(eseguiVariabiliDiSistema.getVariabileDiSistema().getValore().equals("false")){
return null;
}else{
return null;
}
}
}catch (Exception e){
return null;
}
}
}
Starting on Spring Boot 2.6, circular dependencies are prohibited by default. you can allow circular references again by setting the following property:
spring.main.allow-circular-references = true
You can read some more details about this in the Spring Boot 2.6 Release Notes.
The problem that Spring faces here and causes to not able to move forward starting from spring boot 2.6 with the default configuration of spring.main.allow-circular-references = false is located in the following part
#Bean
public BCryptPasswordEncoder metodoCrittografia() {
return new BCryptPasswordEncoder();
}
#Autowired
public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(gestioneUtentiSpringSecurity).passwordEncoder(metodoCrittografia());
}
I believe this is happening because the WebSecurityConfig extends WebSecurityConfigurerAdapter has some circular references in combination with BCryptPasswordEncoder inside this class.
The solution is to create another configuration class, where you can split the configurations so that spring is able to correctly create the beans avoiding circular references.
So you can create the following extra class
#Configuration
public class CustomSecurityConfig {
#Bean
public BCryptPasswordEncoder metodoCrittografia() {
return new BCryptPasswordEncoder();
}
}
Then in your original ConfigurazioneSpringSecurity.class you can replace the failing
#Bean
public BCryptPasswordEncoder metodoCrittografia() {
return new BCryptPasswordEncoder();
}
#Autowired
public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(gestioneUtentiSpringSecurity).passwordEncoder(metodoCrittografia());
}
with the
#Autowired
private PasswordEncoder passwordEncoder;
#Autowired
public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(gestioneUtentiSpringSecurity)
.passwordEncoder(passwordEncoder);
}
Although setting the application.properties works, it is likely using a feature that is going to be deprecated. I was able to work around this by using setter based injection. It's a bit more verbose but might be a good starting point for those looking to stay current and not use features that might be deprecated down the line.
It's certainly an answer that can be improved upon and I hope others can contribute perhaps more concise answers. I'll update this if I find anything cleaner.
Before
#Component
public class CustomFilter extends OncePerRequestFilter {
#Autowired
private MyUserDetailsService myUserDetailsService;
#Autowired
private JWTUtils jwtUtils;
//When any api will be called this method will be called first and this will extract
// Token from header pass to JWT Util calls for token details extraction
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, FilterChain filterChain)
throws ServletException, IOException {
//implementation
}
}
After
#Component
public class CustomFilter extends OncePerRequestFilter {
private MyUserDetailsService myUserDetailsService;
public void setMyUserDetailsService(MyUserDetailsService myUserDetailsService) {
this.myUserDetailsService = myUserDetailsService;
}
public void setJwtUtils(JWTUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
private JWTUtils jwtUtils;
//When any api will be called this method will be called first and this will extract
// Token from header pass to JWT Util calls for token details extraction
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, FilterChain filterChain)
throws ServletException, IOException {
//implementation
}
}
reference:
https://theintuitiveprogrammer.com/post-eight.html
I've this problem during migrate to spring boot 2.6.x with WebSecurityConfig code:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public UserDetailsService userDetailsService() {
return email -> {
....
};
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
...;
}
fix according WebSecurityConfigurerAdapter#userDetailsServiceBean javadoc:
Override this method to expose a UserDetailsService created from configure(AuthenticationManagerBuilder) as a bean ...
To change the instance returned, developers should change userDetailsService() instead
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
#Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
#Override
public UserDetailsService userDetailsService() {
return email -> {
....
};
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
...;
}
I know this question has been asked more than a few times and I have read over all of the answers but am still having an issue. Basically, I am trying to setup an OAuth2 RestTemplate but am receiving the error (like so many others) when attempting to use the RestTemplate to make a getForEntity request:
org.springframework.security.oauth2.client.resource.UserRedirectRequiredException: A redirect is required to get the users approval
Hoping someone can tell me what I am doing wrong. Here is my configuration class:
#Configuration
#EnableOAuth2Client
public class OAuth2Configuration
{
#Autowired
private OAuth2ClientContext oauth2Context;
#Value("${test.oauth.key}")
private String key;
#Value("${test.oauth.secret}")
private String secret;
#Value("${test.auth.url}")
private String authUrl;
#Value("${test.token.url}")
private String tokenUrl;
/**
* Returns an OAuth2 protected resource connection to test
*
* #return the oauth2 protected resource
*/
#Bean
public OAuth2ProtectedResourceDetails test()
{
// Create a new authorization code resource details object
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
// Set the information on the authorization code resource details object
details.setId("java_server");
details.setClientId(key);
details.setClientSecret(secret);
details.setUserAuthorizationUri(authUrl);
details.setAccessTokenUri(tokenUrl);
// Return the authorization code resource details object
return details;
}
/**
* Returns a rest template for test
*
* #return the rest template
*/
#Bean(name = "testRestTemplate")
public OAuth2RestTemplate testRestTemplate()
{
return new OAuth2RestTemplate(test(), oauth2Context);
}
}
This is the beginning of my base test class:
#WebAppConfiguration
public abstract class AbstractJUnitTest extends AbstractJUnit4SpringContextTests
{
#Autowired
protected WebApplicationContext context;
#Autowired
private OAuth2ClientContextFilter clientContextFilter;
#Autowired
private FilterChainProxy springSecurityFilterChain;
// Define protected class variables
protected MockMvc mockMvc;
/**
* Ensure the mock mvc framework has the spring security filter chain in it
*/
#Before
public void setup()
{
// Initialize the mock mvc
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.addFilters(
new RequestContextFilter(),
new DelegatingFilterProxy(clientContextFilter),
new DelegatingFilterProxy(springSecurityFilterChain))
.build();
}
...
}
And I have this line in my http security config:
<security:custom-filter ref="oauth2ClientContextFilter" after="SECURITY_CONTEXT_FILTER" />
Here is my service class that attempts to make the call:
#Service("testService")
public class TestServiceImpl implements TestService
{
#Autowired
#Qualifier("testRestTemplate")
private OAuth2RestTemplate restTemplate;
/**
* ${#inheritDoc}
*/
#Override
public List<Person> listPeople() throws Exception
{
ResponseEntity<String> sample = restTemplate.getForEntity(
myEndpoint, String.class);
return null;
}
}
Any help is appreciated! I am hoping it is just something dumb that I am missing but for whatever reason, I can't see it and therefore need help. Thanks in advance.
I'm looking for ideas how to implement two factor authentication (2FA) with spring security OAuth2. The requirement is that the user needs two factor authentication only for specific applications with sensitive information. Those webapps have their own client ids.
One idea that popped in my mind would be to "mis-use" the scope approval page to force the user to enter the 2FA code/PIN (or whatever).
Sample flows would look like this:
Accessing apps without and with 2FA
User is logged out
User accesses app A which does not require 2FA
Redirect to OAuth app, user logs in with username and password
Redirected back to app A and user is logged in
User accesses app B which also does not require 2FA
Redirect to OAuth app, redirect back to app B and user is directly logged in
User accesses app S which does require 2FA
Redirect to OAuth app, user needs to additionally provide the 2FA token
Redirected back to app S and user is logged in
Directly accessing app with 2FA
User is logged out
User accesses app S which does require 2FA
Redirect to OAuth app, user logs in with username and password, user needs to additionally provide the 2FA token
Redirected back to app S and user is logged in
Do you have other ideas how to apporach this?
So this is how two factor authentication has been implemented finally:
A filter is registered for the /oauth/authorize path after the spring security filter:
#Order(200)
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
#Override
protected void afterSpringSecurityFilterChain(ServletContext servletContext) {
FilterRegistration.Dynamic twoFactorAuthenticationFilter = servletContext.addFilter("twoFactorAuthenticationFilter", new DelegatingFilterProxy(AppConfig.TWO_FACTOR_AUTHENTICATION_BEAN));
twoFactorAuthenticationFilter.addMappingForUrlPatterns(null, false, "/oauth/authorize");
super.afterSpringSecurityFilterChain(servletContext);
}
}
This filter checks if the user hasn't already authenticated with a 2nd factor (by checking if the ROLE_TWO_FACTOR_AUTHENTICATED authority isn't available) and creates an OAuth AuthorizationRequest which is put into the session. The user is then redirected to the page where he has to enter the 2FA code:
/**
* Stores the oauth authorizationRequest in the session so that it can
* later be picked by the {#link com.example.CustomOAuth2RequestFactory}
* to continue with the authoriztion flow.
*/
public class TwoFactorAuthenticationFilter extends OncePerRequestFilter {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private OAuth2RequestFactory oAuth2RequestFactory;
#Autowired
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsService);
}
private boolean twoFactorAuthenticationEnabled(Collection<? extends GrantedAuthority> authorities) {
return authorities.stream().anyMatch(
authority -> ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED.equals(authority.getAuthority())
);
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Check if the user hasn't done the two factor authentication.
if (AuthenticationUtil.isAuthenticated() && !AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
/* Check if the client's authorities (authorizationRequest.getAuthorities()) or the user's ones
require two factor authenticatoin. */
if (twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) ||
twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities())) {
// Save the authorizationRequest in the session. This allows the CustomOAuth2RequestFactory
// to return this saved request to the AuthenticationEndpoint after the user successfully
// did the two factor authentication.
request.getSession().setAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME, authorizationRequest);
// redirect the the page where the user needs to enter the two factor authentiation code
redirectStrategy.sendRedirect(request, response,
ServletUriComponentsBuilder.fromCurrentContextPath()
.path(TwoFactorAuthenticationController.PATH)
.toUriString());
return;
} else {
request.getSession().removeAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
}
}
filterChain.doFilter(request, response);
}
private Map<String, String> paramsFromRequest(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
return params;
}
}
The TwoFactorAuthenticationController that handles entering the 2FA-code adds the authority ROLE_TWO_FACTOR_AUTHENTICATED if the code was correct and redirects the user back to the /oauth/authorize endpoint.
#Controller
#RequestMapping(TwoFactorAuthenticationController.PATH)
public class TwoFactorAuthenticationController {
private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationController.class);
public static final String PATH = "/secure/two_factor_authentication";
#RequestMapping(method = RequestMethod.GET)
public String auth(HttpServletRequest request, HttpSession session, ....) {
if (AuthenticationUtil.isAuthenticatedWithAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
LOG.info("User {} already has {} authority - no need to enter code again", ROLE_TWO_FACTOR_AUTHENTICATED);
throw ....;
}
else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {
LOG.warn("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
throw ....;
}
return ....; // Show the form to enter the 2FA secret
}
#RequestMapping(method = RequestMethod.POST)
public String auth(....) {
if (userEnteredCorrect2FASecret()) {
AuthenticationUtil.addAuthority(ROLE_TWO_FACTOR_AUTHENTICATED);
return "forward:/oauth/authorize"; // Continue with the OAuth flow
}
return ....; // Show the form to enter the 2FA secret again
}
}
A custom OAuth2RequestFactory retrieves the previously saved AuthorizationRequest from the session if available and returns that or creates a new one if none can be found in the session.
/**
* If the session contains an {#link AuthorizationRequest}, this one is used and returned.
* The {#link com.example.TwoFactorAuthenticationFilter} saved the original AuthorizationRequest. This allows
* to redirect the user away from the /oauth/authorize endpoint during oauth authorization
* and show him e.g. a the page where he has to enter a code for two factor authentication.
* Redirecting him back to /oauth/authorize will use the original authorizationRequest from the session
* and continue with the oauth authorization.
*/
public class CustomOAuth2RequestFactory extends DefaultOAuth2RequestFactory {
public static final String SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME = "savedAuthorizationRequest";
public CustomOAuth2RequestFactory(ClientDetailsService clientDetailsService) {
super(clientDetailsService);
}
#Override
public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession(false);
if (session != null) {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
if (authorizationRequest != null) {
session.removeAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
return authorizationRequest;
}
}
return super.createAuthorizationRequest(authorizationParameters);
}
}
This custom OAuth2RequestFactory is set to the authorization server like:
<bean id="customOAuth2RequestFactory" class="com.example.CustomOAuth2RequestFactory">
<constructor-arg index="0" ref="clientDetailsService" />
</bean>
<!-- Configures the authorization-server and provides the /oauth/authorize endpoint -->
<oauth:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenServices"
user-approval-handler-ref="approvalStoreUserApprovalHandler" redirect-resolver-ref="redirectResolver"
authorization-request-manager-ref="customOAuth2RequestFactory">
<oauth:authorization-code authorization-code-services-ref="authorizationCodeServices"/>
<oauth:implicit />
<oauth:refresh-token />
<oauth:client-credentials />
<oauth:password />
</oauth:authorization-server>
When using java config you can create a TwoFactorAuthenticationInterceptor instead of the TwoFactorAuthenticationFilter and register it with an AuthorizationServerConfigurer with
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig implements AuthorizationServerConfigurer {
...
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.addInterceptor(twoFactorAuthenticationInterceptor())
...
.requestFactory(customOAuth2RequestFactory());
}
#Bean
public HandlerInterceptor twoFactorAuthenticationInterceptor() {
return new TwoFactorAuthenticationInterceptor();
}
}
The TwoFactorAuthenticationInterceptor contains the same logic as the TwoFactorAuthenticationFilter in its preHandle method.
I couldn't make the accepted solution work. I have been working on this for a while, and finally I wrote my solution by using the ideas explained here and on this thread "null client in OAuth2 Multi-Factor Authentication"
Here is the GitHub location for the working solution for me:
https://github.com/turgos/oauth2-2FA
I appreciate if you share your feedback in case you see any issues or better approach.
Below you can find the key configuration files for this solution.
AuthorizationServerConfig
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private ClientDetailsService clientDetailsService;
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("ClientId")
.secret("secret")
.authorizedGrantTypes("authorization_code")
.scopes("user_info")
.authorities(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED)
.autoApprove(true);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.requestFactory(customOAuth2RequestFactory());
}
#Bean
public DefaultOAuth2RequestFactory customOAuth2RequestFactory(){
return new CustomOAuth2RequestFactory(clientDetailsService);
}
#Bean
public FilterRegistrationBean twoFactorAuthenticationFilterRegistration(){
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(twoFactorAuthenticationFilter());
registration.addUrlPatterns("/oauth/authorize");
registration.setName("twoFactorAuthenticationFilter");
return registration;
}
#Bean
public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter(){
return new TwoFactorAuthenticationFilter();
}
}
CustomOAuth2RequestFactory
public class CustomOAuth2RequestFactory extends DefaultOAuth2RequestFactory {
private static final Logger LOG = LoggerFactory.getLogger(CustomOAuth2RequestFactory.class);
public static final String SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME = "savedAuthorizationRequest";
public CustomOAuth2RequestFactory(ClientDetailsService clientDetailsService) {
super(clientDetailsService);
}
#Override
public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession(false);
if (session != null) {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
if (authorizationRequest != null) {
session.removeAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
LOG.debug("createAuthorizationRequest(): return saved copy.");
return authorizationRequest;
}
}
LOG.debug("createAuthorizationRequest(): create");
return super.createAuthorizationRequest(authorizationParameters);
}
}
WebSecurityConfig
#EnableResourceServer
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
#Autowired
CustomDetailsService customDetailsService;
#Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
#Bean(name = "authenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/webjars/**");
web.ignoring().antMatchers("/css/**","/fonts/**","/libs/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception { // #formatter:off
http.requestMatchers()
.antMatchers("/login", "/oauth/authorize", "/secure/two_factor_authentication","/exit", "/resources/**")
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin().loginPage("/login")
.permitAll();
} // #formatter:on
#Override
#Autowired // <-- This is crucial otherwise Spring Boot creates its own
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth//.parentAuthenticationManager(authenticationManager)
// .inMemoryAuthentication()
// .withUser("demo")
// .password("demo")
// .roles("USER");
auth.userDetailsService(customDetailsService).passwordEncoder(encoder());
}
}
TwoFactorAuthenticationFilter
public class TwoFactorAuthenticationFilter extends OncePerRequestFilter {
private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationFilter.class);
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private OAuth2RequestFactory oAuth2RequestFactory;
//These next two are added as a test to avoid the compilation errors that happened when they were not defined.
public static final String ROLE_TWO_FACTOR_AUTHENTICATED = "ROLE_TWO_FACTOR_AUTHENTICATED";
public static final String ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED = "ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED";
#Autowired
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsService);
}
private boolean twoFactorAuthenticationEnabled(Collection<? extends GrantedAuthority> authorities) {
return authorities.stream().anyMatch(
authority -> ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED.equals(authority.getAuthority())
);
}
private Map<String, String> paramsFromRequest(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
return params;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Check if the user hasn't done the two factor authentication.
if (isAuthenticated() && !hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
/* Check if the client's authorities (authorizationRequest.getAuthorities()) or the user's ones
require two factor authentication. */
if (twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) ||
twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities())) {
// Save the authorizationRequest in the session. This allows the CustomOAuth2RequestFactory
// to return this saved request to the AuthenticationEndpoint after the user successfully
// did the two factor authentication.
request.getSession().setAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME, authorizationRequest);
LOG.debug("doFilterInternal(): redirecting to {}", TwoFactorAuthenticationController.PATH);
// redirect the the page where the user needs to enter the two factor authentication code
redirectStrategy.sendRedirect(request, response,
TwoFactorAuthenticationController.PATH
);
return;
}
}
LOG.debug("doFilterInternal(): without redirect.");
filterChain.doFilter(request, response);
}
public boolean isAuthenticated(){
return SecurityContextHolder.getContext().getAuthentication().isAuthenticated();
}
private boolean hasAuthority(String checkedAuthority){
return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().anyMatch(
authority -> checkedAuthority.equals(authority.getAuthority())
);
}
}
TwoFactorAuthenticationController
#Controller
#RequestMapping(TwoFactorAuthenticationController.PATH)
public class TwoFactorAuthenticationController {
private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationController.class);
public static final String PATH = "/secure/two_factor_authentication";
#RequestMapping(method = RequestMethod.GET)
public String auth(HttpServletRequest request, HttpSession session) {
if (isAuthenticatedWithAuthority(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED)) {
LOG.debug("User {} already has {} authority - no need to enter code again", TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED);
//throw ....;
}
else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {
LOG.debug("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
//throw ....;
}
LOG.debug("auth() HTML.Get");
return "loginSecret"; // Show the form to enter the 2FA secret
}
#RequestMapping(method = RequestMethod.POST)
public String auth(#ModelAttribute(value="secret") String secret, BindingResult result, Model model) {
LOG.debug("auth() HTML.Post");
if (userEnteredCorrect2FASecret(secret)) {
addAuthority(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED);
return "forward:/oauth/authorize"; // Continue with the OAuth flow
}
model.addAttribute("isIncorrectSecret", true);
return "loginSecret"; // Show the form to enter the 2FA secret again
}
private boolean isAuthenticatedWithAuthority(String checkedAuthority){
return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().anyMatch(
authority -> checkedAuthority.equals(authority.getAuthority())
);
}
private boolean addAuthority(String authority){
Collection<SimpleGrantedAuthority> oldAuthorities = (Collection<SimpleGrantedAuthority>)SecurityContextHolder.getContext().getAuthentication().getAuthorities();
SimpleGrantedAuthority newAuthority = new SimpleGrantedAuthority(authority);
List<SimpleGrantedAuthority> updatedAuthorities = new ArrayList<SimpleGrantedAuthority>();
updatedAuthorities.add(newAuthority);
updatedAuthorities.addAll(oldAuthorities);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
SecurityContextHolder.getContext().getAuthentication().getPrincipal(),
SecurityContextHolder.getContext().getAuthentication().getCredentials(),
updatedAuthorities)
);
return true;
}
private boolean userEnteredCorrect2FASecret(String secret){
/* later on, we need to pass a temporary secret for each user and control it here */
/* this is just a temporary way to check things are working */
if(secret.equals("123"))
return true;
else;
return false;
}
}
Excuse me ! I have a question here. Which version of Spring is “org.springframework.session.*” ? I can't find it in Spring4.0 jar at all.
here is the class:
public abstract class AbstractSessionWebSocketMessageBrokerConfigurer<S extends ExpiringSession>
extends AbstractWebSocketMessageBrokerConfigurer {
#Autowired
#SuppressWarnings("rawtypes")
private SessionRepository sessionRepository;
#Autowired
private ApplicationEventPublisher eventPublisher;
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(sessionRepositoryInterceptor());
}
#Override
public final void registerStompEndpoints(StompEndpointRegistry registry) {
configureStompEndpoints(new SessionStompEndpointRegistry(registry,sessionRepositoryInterceptor()));
}
/**
* Register STOMP endpoints mapping each to a specific URL and (optionally)
* enabling and configuring SockJS fallback options with a
* {#link SessionRepositoryMessageInterceptor} automatically added as an
* interceptor.
*
* #param registry
* the {#link StompEndpointRegistry} which automatically has a
* {#link SessionRepositoryMessageInterceptor} added to it.
*/
protected abstract void configureStompEndpoints(StompEndpointRegistry registry);
#Override
public void configureWebSocketTransport(
WebSocketTransportRegistration registration) {
registration.addDecoratorFactory(wsConnectHandlerDecoratorFactory());
}
#Bean
public WebSocketRegistryListener webSocketRegistryListener() {
return new WebSocketRegistryListener();
}
#Bean
public WebSocketConnectHandlerDecoratorFactory wsConnectHandlerDecoratorFactory() {
return new WebSocketConnectHandlerDecoratorFactory(eventPublisher);
}
#Bean
#SuppressWarnings("unchecked")
public SessionRepositoryMessageInterceptor<S> sessionRepositoryInterceptor() {
return new SessionRepositoryMessageInterceptor<S>(sessionRepository);
}
static class SessionStompEndpointRegistry implements StompEndpointRegistry {
private final StompEndpointRegistry registry;
private final HandshakeInterceptor interceptor;
public SessionStompEndpointRegistry(StompEndpointRegistry registry,HandshakeInterceptor interceptor) {
this.registry = registry;
this.interceptor = interceptor;
}
public StompWebSocketEndpointRegistration addEndpoint(String... paths) {
StompWebSocketEndpointRegistration endpoints = registry.addEndpoint(paths);
endpoints.addInterceptors(interceptor);
return endpoints;
}
}
}
Spring Session is a separate project: https://github.com/spring-projects/spring-session.
You should use some dependency management tool (Gradle or Maven) to have a control over artifacts for your application.
See WebScoket sample there: https://github.com/spring-projects/spring-session/tree/master/samples/websocket .
A Spring Session artifact is:
compile "org.springframework.session:spring-session:1.0.0.RC1"