ProviderManager.authenticate called twice for BadCredentialsException - spring

Spring 4.1 and Spring Security 3.2:
We implemented a Custom Authentication Provider, that throws a BadCredentialsException if user enters an incorrect password.
When the BadCredentialsException is thrown, the ProviderManager.authenticate method is called, which calls the authenticate method in the Custom Authentication again. When a LockedException is thrown, the authenicate method in the Custom Authentication Provider is not called again. We are planning on keeping a count of number of login attempts, so we don't want the authenticate method called twice. Does anyone know why the authenticate method in the custom authentication class would be called twice?
WebConfig:
#Configuration
#EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
#Autowired
private AMCiUserDetailsService userDetailsService;
#Autowired
private CustomImpersonateFailureHandler impersonateFailureHandler;
#Autowired
private LoginFailureHandler loginFailureHandler;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/jsp/*.css","/jsp/*.js","/images/**").permitAll()
.antMatchers("/login/impersonate*").access("hasRole('ADMIN') or hasRole('ROLE_PREVIOUS_ADMINISTRATOR')")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.jsp")
.defaultSuccessUrl("/jsp/Home.jsp",true)
.loginProcessingUrl("/login.jsp")
.failureHandler(loginFailureHandler)
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/login.jsp?msg=1")
.permitAll()
.and()
.addFilter(switchUserFilter())
.authenticationProvider(customAuthenticationProvider);
http.exceptionHandling().accessDeniedPage("/jsp/SecurityViolation.jsp"); //if user not authorized to a page, automatically forward them to this page.
http.headers().addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN));
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider);
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//Used for the impersonate functionality
#Bean CustomSwitchUserFilter switchUserFilter() {
CustomSwitchUserFilter filter = new CustomSwitchUserFilter();
filter.setUserDetailsService(userDetailsService);
filter.setTargetUrl("/jsp/Impersonate.jsp?msg=0");
filter.setSwitchUserUrl("/login/impersonate");
filter.setExitUserUrl("/logout/impersonate");
filter.setFailureHandler(impersonateFailureHandler);
return filter;
}
}
Custom Authentication Provider:
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired(required = true)
private HttpServletRequest request;
#Autowired
private AMCiUserDetailsService userService;
#Autowired
private PasswordEncoder encoder;
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName().trim();
String password = ((String) authentication.getCredentials()).trim();
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
throw new BadCredentialsException("Login failed! Please try again.");
}
UserDetails user;
try {
user = userService.loadUserByUsername(username);
//log successful attempt
auditLoginBean.setComment("Login Successful");
auditLoginBean.insert();
} catch (Exception e) {
try {
//log unsuccessful attempt
auditLoginBean.setComment("Login Unsuccessful");
auditLoginBean.insert();
} catch (Exception e1) {
// TODO Auto-generated catch block
}
throw new BadCredentialsException("Please enter a valid username and password.");
}
if (!encoder.matches(password, user.getPassword().trim())) {
throw new BadCredentialsException("Please enter a valid username and password.");
}
if (!user.isEnabled()) {
throw new DisabledException("Please enter a valid username and password.");
}
if (!user.isAccountNonLocked()) {
throw new LockedException("Account locked. ");
}
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
List<GrantedAuthority> permlist = new ArrayList<GrantedAuthority>(authorities);
return new UsernamePasswordAuthenticationToken(user, password, permlist);
}
public boolean supports(Class<? extends Object> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}

The reason is that you add your authentication provider twice, one time in configure(HttpSecurity) and one time in configure(AuthenticationManagerBuilder). This will create a ProviderManager with two items, both being your provider.
When authentication is processed, the providers will be asked in order until a success is made, unless a LockedException or similar status exception is thrown, then the loop will break.

There may be a situtation which you don't override configure(AuthenticationManagerBuilder) and still same AuthenticationProver's authenticate method gets called twice like Phil mentioned in his comment in the accepted answer.
Why is that?
The reason is that when you don't override configure(AuthenticationManagerBuilder) and have an AuthenticationProvider bean, it will be registered by Spring Security, you don't have to do anything else.
However, when configure(AuthenticationManagerBuilder) overridden, Spring Security will invoke it and won't try to register any provider by itself.
If you're curious, you can take a look the related method. disableLocalConfigureAuthenticationBldr is true if you override configure(AuthenticationManagerBuilder).
So, briefly, if you want to register just one custom AuthenticationProvider then do not override configure(AuthenticationManagerBuilder), do not call authenticationProvider(AuthenticationProvider) in configure(HttpSecurity), just make your AuthenticationProviver implementation bean by annotating #Component and you're good to go.

Related

Configuring both http basic and Form based authentication in latest Spring security 6.0

I'm trying to configure REST and Form based authentication in new Spring boot and spring secuirty latest. I went through following post Combining basic authentication and form login for the same REST Api and new spring configuration using SecurityFilterChain and created the following. As I learnt that WebSecurityConfigurerAdapter method in the above post is deprecated.
#Configuration
#Order(2)
#EnableWebSecurity
public class RESTBasedConfigurationAdapter {
#Autowired
private AuthenticationProvider authenticationProvider;
#Bean
public BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
#Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.antMatcher("/api/**")
.authorizeRequests().anyRequest().hasAnyRole(...)
.and().httpBasic()
.authenticationEntryPoint(authenticationEntryPoint());
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return http.build();
}
}
and
#Configuration
#Order(Ordered.HIGHEST_PRECEDENCE)
#EnableWebSecurity
public class FormBasedConfigurationAdapter {
#Autowired
private AuthenticationProvider authenticationProvider;
#Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
#Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(...)
.permitAll().anyRequest().authenticated()
.and().formLogin().loginPage("/login").successHandler(authenticationSuccessHandler)
.permitAll().and().logout().permitAll();
return http.build();
}
}
But the configure method FormBasedConfigurationAdapter's is never called. Please explain how to configure so that both http Basic and Form based Authentication can be done.
As far I under I want two flows.
One flow REST which uses the following AuthenticationProvider for logging STATELESS
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
#Lazy
private BCryptPasswordEncoder passwordEncoder;
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String userName = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails userDetails = userDetailsService.loadUserByUsername(userName);
if (passwordEncoder.matches(password, userDetails.getPassword())) {
return new UsernamePasswordAuthenticationToken(userName, password, userDetails.getAuthorities());
} else {
throw new BadCredentialsException(" Bad Credentials ");
}
}
#Override
public boolean supports(Class<?> authenticationType) {
return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
}
}
and for other FormBased authentication, I would like to go through controller as below.
#PostMapping("/login")
public String login(#ModelAttribute("loginForm") LoginForm loginForm,
BindingResult bindingResult,
Model model) {
loginValidator.validate(loginForm, bindingResult);
securityService.login(loginForm.getUserName(), loginForm.getPasswd());
if (bindingResult.hasErrors()) {
return "login";
}
return "...";
}
to facilitate Validation for Authenticate using Service as below.
#Override
public void login(String username, String password) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
if (passwordEncoder.matches(password, userDetails.getPassword())) {
if (usernamePasswordAuthenticationToken.isAuthenticated()) {
SecurityContextHolder.getContext()
.setAuthentication(usernamePasswordAuthenticationToken);
logger.debug(String.format("Auto login %s successfully!", username));
}
} else {
throw new BadCredentialsException(" Bad Credentials ");
}
}
Please explain how to achieve this. I also tried doing both HttpSecurity mapping in the same class but it is not working due to various reasons.

spring boot security custom successHandler with rest not working

not sure if my question is good..
Perhaps I was looking very badly for information about the spring security
In general, I hope it will not be difficult for you to answer.
The question is, I use spring security with my login page. The login page is just in the public templates folder. I do not create a separate Controller for it that would return the view page (would it be correct to create a controller for it that would return the view login page?). In any case, my code works even without this page view controller. But only my custom SuccessHandler does not work (which, after login, checks by roles and would redirect to another page).
Should I redirect by role to the appropriate pages using a different approach? (I mean if ADMIN_ROLE after login is redirected to the admin-panel.html)
my security
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class CustomWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private UserServiceImpl userServiceImpl;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
.authorizeRequests()
.antMatchers("/", "/templates/sign-up.html").permitAll()
.antMatchers("/api/users", "/api/users/login").permitAll()
.antMatchers("/templates/admin-panel.html").hasRole("ADMIN")
.antMatchers("/all-users").hasRole("ADMIN")
.antMatchers("/news").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/templates/login.html")
.defaultSuccessUrl("/")
.permitAll()
.successHandler(myAuthenticationSuccessHandler())
.and()
.logout()
.permitAll()
.logoutSuccessUrl("/index.html");
http.csrf().disable();
}
#Override
public void configure(WebSecurity web) {
web
.ignoring()
.antMatchers("/css/**")
.antMatchers("/js/**")
.antMatchers("/static/**")
.antMatchers("/resources/**");
}
#Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceImpl).passwordEncoder(bCryptPasswordEncoder());
}
#Bean
public AuthenticationSuccessHandler myAuthenticationSuccessHandler(){
return new CustomAuthenticationSuccessHandler();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
my custom success handler
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public CustomAuthenticationSuccessHandler() {
super();
}
// API
#Override
public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException {
handle(request, response, authentication);
clearAuthenticationAttributes(request);
}
// IMPL
protected void handle(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException {
final String targetUrl = determineTargetUrl(authentication);
if (response.isCommitted()) {
logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
return;
}
redirectStrategy.sendRedirect(request, response, targetUrl);
}
protected String determineTargetUrl(final Authentication authentication) {
Map<String, String> roleTargetUrlMap = new HashMap<>();
roleTargetUrlMap.put("ROLE_USER", "/index.html");
roleTargetUrlMap.put("ROLE_ADMIN", "/templates/admin-panel.html");
final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (final GrantedAuthority grantedAuthority : authorities) {
String authorityName = grantedAuthority.getAuthority();
if(roleTargetUrlMap.containsKey(authorityName)) {
return roleTargetUrlMap.get(authorityName);
}
}
throw new IllegalStateException();
}
/**
* Removes temporary authentication-related data which may have been stored in the session
* during the authentication process.
*/
protected final void clearAuthenticationAttributes(final HttpServletRequest request) {
final HttpSession session = request.getSession(false);
if (session == null) {
return;
}
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
}
my controller
#CrossOrigin
#RestController
#RequestMapping("/api/users")
public class UserController {
private final UserServiceImpl userService;
private AuthenticationManager authenticationManager;
public UserController(UserServiceImpl userService, AuthenticationManager authenticationManager) {
this.userService = userService;
this.authenticationManager = authenticationManager;
}
#PostMapping
public ResponseEntity<?> register(#RequestBody UserDTO user) {
try {
userService.register(user);
return new ResponseEntity<>("User added", HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(e, HttpStatus.BAD_REQUEST);
}
}
#PostMapping(value = "/login")
public ResponseEntity<?> login(#RequestBody UserDTO user, HttpServletResponse response) {
try {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
boolean isAuthenticated = isAuthenticated(authentication);
if (isAuthenticated) {
SecurityContextHolder.getContext().setAuthentication(authentication);
// response.sendRedirect("/templates/admin-panel.html");
// my pathetic attempt to create a redirect to another page
}
return new ResponseEntity<>("user authenticated", HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(e, HttpStatus.FORBIDDEN);
}
}
private boolean isAuthenticated(Authentication authentication) {
return authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated();
}
my static files
enter image description here
My Guess, as you didn't post your login page itself:
You don't need a controller listening to POST /login this normally automatically registered by Spring Security with all security related authentication stuff. No need to try it by yourself as in UserController.login(). I guess by regsitering this endpoint you override / deactivate the regular spring security behaviour.
Normally you just need a login page with a form that posts correctly to /login. The handling on the backend side is done by spring security itself.
See https://spring.io/guides/gs/securing-web/ for a minimal worling setup.

refresh_token grant_type error: UserDetailsService is required. But I dont want to specify one

I'm trying to create an Oauth authentication/authorization server using spring boot and dependencies
* spring-security-oauth2-autoconfigure
* nimbus-jose-jwt
and I'm following docs.spring.io/spring-security-oauth2-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-security-oauth2-authorization-server
The issue is that I don't want to specify a UserDetailsService since the information about the user account is in another service that doesn't expose passwords. That service just has an API in which input is user/pass and output is user info (if the user exists/credentials are correct).
So my code/configuration is a little deviated from the documentation.
#EnableAuthorizationServer
#Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
//injections
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenStore(jwtTokenStore)
.accessTokenConverter(accessTokenConverter)
.authenticationManager(authenticationManager);
}
}
and
#EnableWebSecurity
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//injections
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
authenticationManagerBuilder.authenticationProvider(travelerAuthenticationProvider); //my custom // authentication provider that calls the other service for checking credentials
}
}
and
#Component
public class TravelerAuthenticationProvider implements AuthenticationProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(TravelerAuthenticationProvider.class);
private OrderTravelerProfileClient travelerProfileClient;
public TravelerAuthenticationProvider(OrderTravelerProfileClient travelerProfileClient) {
this.travelerProfileClient = travelerProfileClient;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication.getName() == null || (authentication.getCredentials().toString().isEmpty())) {
return null;
}
var username = authentication.getName();
var password = authentication.getCredentials().toString();
try {
travelerProfileClient.authenticate(username, password);
} catch (Exception e) {
LOGGER.error("checking traveler {} credentials failed", username, e);
throw new BadCredentialsException("wrong traveler credentials");
}
var authorities = Set.of(new SimpleGrantedAuthority("traveler"));
var updatedAuthentication = new UsernamePasswordAuthenticationToken(username, password, authorities);
return updatedAuthentication;
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
Everything related to client_credentials and password flow works but when I try to use refresh_token flow, it complains that UserDetailsService is required. How should I solve the issue without defining a UserDetailsService and just relaying on my custom authentication provider?
UPDATE:
apparently refresh_token flow has a recheck for authentication (credentials) which needs another authentication provider for type PreAuthenticatedAuthenticationToken.class.
So I created a new auth provider like this:
#Component
public class TravelerRefreshTokenBasedAuthenticationProvider implements AuthenticationProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(TravelerRefreshTokenBasedAuthenticationProvider.class);
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
var currentAuthentication = (PreAuthenticatedAuthenticationToken) authentication;
//.....
return updatedAuthentication;
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(PreAuthenticatedAuthenticationToken.class);
}
}
and update my security configs to:
#EnableWebSecurity
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//injections
//this bean will be more configured by the method below and it will be used by spring boot
//for authenticating requests. Its kind of an equivalent to userDetailsService
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
authenticationManagerBuilder.authenticationProvider(travelerUserPassBasedAuthenticationProvider);
authenticationManagerBuilder.authenticationProvider(travelerRefreshTokenBasedAuthenticationProvider);
}
}
the issue is spring doesn't recognize my auth providers in refresh_token flow and tries to use a default one. And the default one is trying to use a UserDetailsService that doesn't exist.
I also feel that I don't need to create another provider and I can reuse the previous one. Because the check for which spring is failing to use my custom provider is a check against user/pass; which I was doing in my previous auth provider.
so all in all, until now, I feel I have to introduce my custom provider to spring differently for refresh_token flow comparing to password flow
Your AuthenticationProvider implementation only supports UsernamePasswordAuthenticationToken, which is used for username/password authentication, while the refresh_token flow tries to renew authentication using PreAuthenticatedAuthenticationToken (see DefaultTokenServices.java).
So you need to create another AuthenticationProvider for PreAuthenticatedAuthenticationToken and add it to AuthenticationManagerBuilder.
Update:
I've found that AuthorizationServerEndpointsConfigurer creates a new instance of DefaultTokenServices, if none is assigned, which in turn creates a new instance of PreAuthenticatedAuthenticationProvider and does not use the provided AuthenticationManager. To avoid this, you can create your own instance of DefaultTokenServices and pass it to AuthorizationServerEndpointsConfigurer:
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenStore(jwtTokenStore)
.accessTokenConverter(accessTokenConverter)
.tokenEnhancer(accessTokenConverter)
.authenticationManager(authenticationManager)
.tokenServices(createTokenServices(endpoints, authenticationManager));
}
private DefaultTokenServices createTokenServices(AuthorizationServerEndpointsConfigurer endpoints, AuthenticationManager authenticationManager) {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAuthenticationManager(authenticationManager);
return tokenServices;
}

Oauth Spring Security not unauthorized to microservice oauth, registerController

I am beginner to Spring. I've been with this problem for several days and I wouldn't know how to solve it.
I have two microservices:
Microservice authentication : This allows return a JWT token to user when it access to path: /oauth/token
Microservice Account user : This microservice will have a lot of functions but the problem is it:
The user must be register in platform and this microservice call to server oauth to save the new created user.
So, I create the controller to microservice Oauth:
#PostMapping
#PreAuthorize("#oauth2.hasScope('server')")
public UserDto createUser(#Valid #RequestBody UserDto userDto) {
Usuario savedUser = new Usuario();
try {
savedUser = userService.create(this.toUsuario(userDto));
} catch (ArendheException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return toDto(savedUser);
}
The WebSecurityConfigureAdapter is:
#Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService usuarioService;
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
#Autowired
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.usuarioService).passwordEncoder(passwordEncoder());
}
#Bean("authenticationManager")
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.POST,"/oauth/**").permitAll()
.antMatchers(HttpMethod.POST, "/user/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
As you can see, I write two antMatchers, the second to create a new user.
The problem is when I test it with Postman (path localhost:8005/user with data JSON parsed to POST method). The output is:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
I dont understand it because I have a permitAll to /user/** path.
Thanks.

Spring Security REST Login

I got question about login with REST API with Spring Security. As far as login with default login window provided by Spring Security is working and it is authenticating with Database, I have no idea how to make my own login. I know how to substitute the form for my own, but where should I send the data? Should I POST it for some address? I made basic form with username and password.
Try this one, it might help you... at least to understand what you are missing.
This code is not guarantee to be worked 100%, some part is intentionally missed (error handling and it's format, loading user, some checks, Session API).
The basic idea is you must to register a filter (react on all secured request for authentication process) and a provider that later on will be able to load authonticated user and create for you security context (e.g. you know each request is handled per thread and this user can be obtained by SecurityContextHolder/ThreadLocal).
And you need to create a separate controller to handle the initial case for creating a user session aka login/Authorization. Response of this API must to contain some session's GUID to use it as value of header later on: Authentication: Bearer <value>
some spec: https://www.rfc-editor.org/rfc/rfc6750
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)//optional
#Import(RestSecurityConfig.TokenAuthenticationProvider.class)// one of the way to create spring bean
public class RestSecurityConfig extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
new AntPathRequestMatcher("/actuator/*"),
new AntPathRequestMatcher("/some_api_to_login", POST), // this must be public
);
private static final RequestMatcher PROTECTED_URLS = new NegatedRequestMatcher(PUBLIC_URLS);
// better to move it out as a separate class
public static class TokenAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
#Override
public boolean supports(Class<?> authentication) {
return MyAuthenticationToken.class.isAssignableFrom(authentication);
}
#Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
}
#Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
return null; // service/dao.loadUser
}
}
public static class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public TokenAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
super(requiresAuthenticationRequestMatcher);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
Authentication auth = new MyAuthenticationToken(request.getHeader("Authentication"));
return getAuthenticationManager().authenticate(auth);
}
}
#Autowired
TokenAuthenticationProvider authenticationProvider;
#Override
protected void configure(final AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authenticationProvider);
}
#Override
public void configure(final WebSecurity web) {
web.ignoring().requestMatchers(PUBLIC_URLS);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// maybe some of the tuning you might not need
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.exceptionHandling()
.defaultAuthenticationEntryPointFor(new Http403ForbiddenEntryPoint(), PROTECTED_URLS).and()
.authorizeRequests().anyRequest().authenticated().and()
.cors().and()
.anonymous().disable()
.rememberMe().disable()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable();
// it's important
http.addFilterBefore(tokenAuthenticationFilter(), AnonymousAuthenticationFilter.class);
}
#Bean
AbstractAuthenticationProcessingFilter tokenAuthenticationFilter() throws Exception {
final AbstractAuthenticationProcessingFilter filter = new TokenAuthenticationFilter(PROTECTED_URLS);
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(successHandler());
// maybe error handling to provide some custom response?
return filter;
}
// it's critically important to register your filter properly in spring context
/** Disable Spring boot automatic filter registration. */
#Bean
FilterRegistrationBean disableRegistrationForAuthenticationFilter(final TokenAuthenticationFilter filter) {
final FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
// this one also is critically important to avoid redirection
#Bean
SimpleUrlAuthenticationSuccessHandler successHandler() {
final SimpleUrlAuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler();
successHandler.setRedirectStrategy(new NoRedirectStrategy());
return successHandler;
}
}
You can store usernames and passwords in database, which you can use to login users. You create your own class which extends WebSecurityConfigurerAdapter and override methods which you need to modify:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
DataSource dataSource;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.jdbcAuthentication()
.dataSource(dataSource)
}
}
But be vary of Spring Security default database query when searching for usernames and passwords so you can create database schema which will be good:
public static final String DEF_USERS_BY_USERNAME_QUERY =
"select username,password,enabled " +
"from users " +
"where username = ?";
public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =
"select username,authority " +
"from authorities " +
"where username = ?";
public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY =
"select g.id, g.group_name, ga.authority " +
"from groups g, group_members gm, group_authorities ga " +
"where gm.username = ? " +
"and g.id = ga.group_id " +
"and g.id = gm.group_id";
But you can also use Spring methods to specify your own query to database:
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select username, password, enabled from Users " +
"where username=?")
You should POST your data to some service you created which will store user and pass to a database.

Resources