I'm new to the Spring framework, so I apologize in advance for any gaping holes in my understanding.
I'm using Auth0 to secure my API, which works perfectly. My setup & config is the same as the suggested setup in the Auth0 documentation:
// SecurityConfig.java
#Configuration
#EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// auth0 config vars here
#Override
protected void configure(HttpSecurity http) {
JwtWebSecurityConfigurer
.forRS256(apiAudience, issuer)
.configure(http)
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/public").permitAll()
.antMatchers(HttpMethod.GET, "/api/private").authenticated();
}
}
With this setup, the spring security principal is being set to the userId (sub) from the jwt token: auth0|5b2b.... However, instead of just the userId, I want it set to the matching user (from my database). My question is how to do that.
What I've tried
I've tried implementing a custom database-backed UserDetailsService that I copied from this tutorial.
However, it's not getting called regardless of how I try to add it to my conf. I've tried adding it several different ways with no effect:
// SecurityConfig.java (changes only)
// My custom userDetailsService, overriding the loadUserByUsername
// method from Spring Framework's UserDetailsService.
#Autowired
private MyUserDetailsService userDetailsService;
protected void configure(HttpSecurity http) {
http.userDetailsService(userDetailsService); // Option 1
http.authenticationProvider(authenticationProvider()); // Option 2
JwtWebSecurityConfigurer
[...] // The rest unchanged from above
}
#Override // Option 3 & 4: Override the following method
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authenticationProvider()); // Option 3
auth.userDetailsService(userDetailsService); // Option 4
}
#Bean // Needed for Options 2 or 4
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
return authProvider;
}
Unfortunately none of the similar "userDetails not being called" questions have helped me due to me needing to combine it with Auth0 authentication.
I'm not positive that I'm on the right path with this. It seems strange to me that I can't find any documentation from Auth0 on this extremely common use case, so maybe I'm missing something obvious.
PS: Not sure if relevant, but the following is always logged during init.
Jun 27, 2018 11:25:22 AM com.test.UserRepository initDao
INFO: No authentication manager set. Reauthentication of users when changing passwords will not be performed.
EDIT 1:
Based on Ashish451's answer, I tried copying his CustomUserDetailsService, and added the following to my SecurityConfig:
#Autowired
private CustomUserDetailsService userService;
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Autowired
public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exception {
auth.userDetailsService( userService );
}
Unfortunately with those changes, CustomUserDetailsService is still not being called.
EDIT 2:
Output when adding the logging method suggested by #Norberto Ritzmann:
Jul 04, 2018 3:49:22 PM com.test.repositories.UserRepositoryImpl initDao
INFO: No authentication manager set. Reauthentication of users when changing passwords will not be performed.
Jul 04, 2018 3:49:22 PM com.test.runner.JettyRunner testUserDetailsImpl
INFO: UserDetailsService implementation: com.test.services.CustomUserDetailsService
Looking at your Adapter code you are generating JWT token in configure itself.
Am not sure whats apiAudience, issuer but it generated sub of JWT I guess.
Your issue is that you want to change JWT sub as per your Database.
I have recently implemented JWT security in Spring Boot Application.
And I am setting UserName after fetching it from Database.
I have added code with pkg info for clarity.
// My adapter class. Its same as your's except one thing that I have added a Filter to it. In this Filter I am authenticating JWT token. This filter will be called each time a Secured Rest URL is fired.
import java.nio.charset.StandardCharsets;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import com.dev.myapp.jwt.model.CustomUserDetailsService;
import com.dev.myapp.security.RestAuthenticationEntryPoint;
import com.dev.myapp.security.TokenAuthenticationFilter;
import com.dev.myapp.security.TokenHelper;
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Autowired
private CustomUserDetailsService jwtUserDetailsService; // Get UserDetail bu UserName
#Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint; // Handle any exception during Authentication
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// Binds User service for User and Password Query from Database with Password Encryption
#Autowired
public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exception {
auth.userDetailsService( jwtUserDetailsService )
.passwordEncoder( passwordEncoder() );
}
#Autowired
TokenHelper tokenHelper; // Contains method for JWT key Generation, Validation and many more...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS ).and()
.exceptionHandling().authenticationEntryPoint( restAuthenticationEntryPoint ).and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated().and()
.addFilterBefore(new TokenAuthenticationFilter(tokenHelper, jwtUserDetailsService), BasicAuthenticationFilter.class);
http.csrf().disable();
}
// Patterns to ignore from JWT security check
#Override
public void configure(WebSecurity web) throws Exception {
// TokenAuthenticationFilter will ignore below paths
web.ignoring().antMatchers(
HttpMethod.POST,
"/auth/login"
);
web.ignoring().antMatchers(
HttpMethod.GET,
"/",
"/assets/**",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
);
}
}
// User Service to get User Details
#Transactional
#Repository
public class CustomUserDetailsService implements UserDetailsService {
protected final Log LOGGER = LogFactory.getLog(getClass());
#Autowired
private UserRepo userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User uu = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
} else {
return user;
}
}
}
// Unauthorized access handler
#Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
// This is invoked when user tries to access a secured REST resource without supplying any credentials
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
// Filter Chain for Validating JWT Token
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.filter.OncePerRequestFilter;
public class TokenAuthenticationFilter extends OncePerRequestFilter {
protected final Log logger = LogFactory.getLog(getClass());
private TokenHelper tokenHelper;
private UserDetailsService userDetailsService;
public TokenAuthenticationFilter(TokenHelper tokenHelper, UserDetailsService userDetailsService) {
this.tokenHelper = tokenHelper;
this.userDetailsService = userDetailsService;
}
#Override
public void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws IOException, ServletException {
String username;
String authToken = tokenHelper.getToken(request);
logger.info("AuthToken: "+authToken);
if (authToken != null) {
// get username from token
username = tokenHelper.getUsernameFromToken(authToken);
logger.info("UserName: "+username);
if (username != null) {
// get user
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (tokenHelper.validateToken(authToken, userDetails)) {
// create authentication
TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails);
authentication.setToken(authToken);
SecurityContextHolder.getContext().setAuthentication(authentication); // Adding Token in Security COntext
}
}else{
logger.error("Something is wrong with Token.");
}
}
chain.doFilter(request, response);
}
}
// TokenBasedAuthentication class
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
public class TokenBasedAuthentication extends AbstractAuthenticationToken {
private static final long serialVersionUID = -8448265604081678951L;
private String token;
private final UserDetails principle;
public TokenBasedAuthentication( UserDetails principle ) {
super( principle.getAuthorities() );
this.principle = principle;
}
public String getToken() {
return token;
}
public void setToken( String token ) {
this.token = token;
}
#Override
public boolean isAuthenticated() {
return true;
}
#Override
public Object getCredentials() {
return token;
}
#Override
public UserDetails getPrincipal() {
return principle;
}
}
// Helper class for JWT generation and Validation Logic
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import com.dev.myapp.common.TimeProvider;
import com.dev.myapp.entity.User;
#Component
public class TokenHelper {
protected final Log LOGGER = LogFactory.getLog(getClass());
#Value("${app.name}") // reading details from property file added in Class path
private String APP_NAME;
#Value("${jwt.secret}")
public String SECRET;
#Value("${jwt.licenseSecret}")
public String LICENSE_SECRET;
#Value("${jwt.expires_in}")
private int EXPIRES_IN;
#Value("${jwt.mobile_expires_in}")
private int MOBILE_EXPIRES_IN;
#Value("${jwt.header}")
private String AUTH_HEADER;
#Autowired
TimeProvider timeProvider; // return current time. Basically Deployment time.
private SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512;
// Generate Token based on UserName. You can Customize this
public String generateToken(String username) {
String audience = generateAudience();
return Jwts.builder()
.setIssuer( APP_NAME )
.setSubject(username)
.setAudience(audience)
.setIssuedAt(timeProvider.now())
.setExpiration(generateExpirationDate())
.signWith( SIGNATURE_ALGORITHM, SECRET )
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
User user = (User) userDetails;
final String username = getUsernameFromToken(token);
final Date created = getIssuedAtDateFromToken(token);
return (
username != null &&
username.equals(userDetails.getUsername())
);
}
// If Token is valid will extract all claim else throw appropriate error
private Claims getAllClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.error("Could not get all claims Token from passed token");
claims = null;
}
return claims;
}
private Date generateExpirationDate() {
long expiresIn = EXPIRES_IN;
return new Date(timeProvider.now().getTime() + expiresIn * 1000);
}
}
For this Log
No authentication manager set. Reauthentication of users when changing passwords
Since you have not implemented a methods with Name loadUserByUsername. You are getting this log.
Edit 1:
I am using Filter Chain just to Validate Token and add User in Security Context which will be extracted from Token....
Am using JWT and you are using AuthO, only Implementation is Different. Am added full implementation for a complete work flow.
You focus on implementing authenticationManagerBean and configureGlobal from WebSecurityConfig class to use UserService.
and TokenBasedAuthentication class implementation.
Other things you can skip.
Maybe this is an spring-boot context initialization issue, meaning the #Autowired annotation cannot be resolved during the initialization of the Configuration class.
You could try the #ComponentScan() annotation on top of your Configuration class and load your MyUserDetailsService explicitly. (see: https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-configuration-classes.html#using-boot-importing-configuration). Having done this I would recommend the following in your Configuration class:
#Autowired
private MyUserDetailsService userService;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
Hope this can help you out.
I ended up asking Auth0 support about this, and they say that it's currently not possible to modify the principal without modifying the library source.
They provide an alternative approach, however, which is to use a JWT validation library (e.g. https://github.com/auth0/java-jwt) instead of their Spring Security API SDK.
My solution will be to modify my code to work with just the token as principal.
You could extend JwtAuthenticationProvider with overridden authenticate method which will put your user into Authentication object:
Using springboot 2.1.7.RELEASE
Auth0 deps: com.auth0:auth0:1.14.2, com.auth0:auth0-spring-security-api:1.2.5, com.auth0:jwks-rsa:0.8.3
Note: some errors in the following code snippets might exist as I've transformed kotlin code by hand into java
Configure SecurityConfig as usual but pass modified authentication provider:
#Autowired UserService userService;
...
#Override
protected void configure(HttpSecurity http) {
// same thing used in usual method `JwtWebSecurityConfigurer.forRS256(String audience, String issuer)`
JwkProvider jwkProvider = JwkProviderBuilder(issuer).build()
// provider deduced from existing default one
Auth0UserAuthenticationProvider authenticationProvider = new Auth0UserAuthenticationProvider(userService, jwkProvider, issuer, audience)
JwtWebSecurityConfigurer
.forRS256(apiAudience, issuer, authenticationProvider)
.configure(http)
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/public").permitAll()
.antMatchers(HttpMethod.GET, "/api/private").authenticated();
}
Extend default JwtAuthenticationProvider which is usually used in method JwtWebSecurityConfigurer.forRS256(String audience, String issuer)
public class Auth0UserAuthenticationProvider extends JwtAuthenticationProvider {
private final UserService userService;
public (UserService userService, JwkProvider jwkProvider, String issuer, String audience) {
super(jwkProvider, issuer, audience);
this.userService = userService;
}
/**
* Intercept Authentication object before it is set in context
*/
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Authentication jwtAuth = super.authenticate(authentication);
// Use your service and get user details here
User user = userService.getDetailsOrWhatever(jwtAuth.getPrincipal().toString());
// TODO implement following class which merges Auth0 provided details with your user
return new MyAuthentication(jwtAuth, user);
}
}
Implement your own MyAuthentication.class which will override getDetails() and return the actual user instead of decoded token given by Auth0 library.
Afterwards user will be available in
SecurityContextHolder.getContext().getAuthentication().getDetails();
This is what I ended up doing if you want to just stick with using spring libraries. You can change Auth0UserDetailsService to query user details from any source.
public class Auth0JwtAuthenticationProvider implements AuthenticationProvider {
#Autowired
ApplicationContext context;
#Autowired
#Qualifier("auth0UserDetailsService")
UserDetailsService auth0UserDetailsService;
private JwtAuthenticationProvider jwtAuthenticationProvider;
public Auth0JwtAuthenticationProvider() {
}
#PostConstruct
public void postConstruct() {
jwtAuthenticationProvider = new JwtAuthenticationProvider(context.getBean(JwtDecoder.class));
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Authentication token = jwtAuthenticationProvider.authenticate(authentication);
((AbstractAuthenticationToken) token).setDetails(auth0UserDetailsService.loadUserByUsername(authentication.getName()));
return token;
}
#Override
public boolean supports(Class<?> authentication) {
return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication);
}
}
And for UserDetailsService I am getting user details using a service.
public class Auth0UserDetailsService implements UserDetailsService {
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
RestTemplate restTemplate = new RestTemplate();
.....
ResponseEntity<Auth0UserDetails> responseEntity = restTemplate.exchange(url, HttpMethod.GET, entity, Auth0UserDetails.class);
return responseEntity.getBody();
}
.....
}
And UserDetails will have all details you need.
public class Auth0UserDetails implements UserDetails {
#JsonProperty("name")
private String userName;
#JsonProperty("created_at")
private LocalDateTime createdAt;
#JsonProperty("email")
private String email;
#JsonProperty("email_verified")
private boolean isEmailVerified;
#JsonProperty("nickname")
private String nickName;
#JsonProperty("picture")
private String pictureURL;
#JsonProperty("updated_at")
private LocalDateTime updatedAt;
#JsonProperty("user_id")
private String userID;
...
//Getters and Setter and overridden methods.
}
#EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public Auth0JwtAuthenticationProvider auth0JwtAuthenticationProvider() {
return new Auth0JwtAuthenticationProvider();
}
#Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authenticationProvider(auth0JwtAuthenticationProvider()).authorizeRequests().
....
.oauth2ResourceServer().jwt();
}}
Related
What I'm going to do is like this.
post additional parameter when user submit on consent page.
send posted additional parameter to redirect uri.
I've checked through AuthorizationRequestConverter that OAuth2AuthorizationCodeRequestAuthenticationToken has the parameter which user has uploaded.
But I don't know why OAuth2Authorization doesn't save additional parameters on Database.
Cause of above I cant' reach to additional parameters and can't post data to redirect URI.
I've referred to this repository.
sjohnr/spring-authorization-server
Here's my auth server configuration
AuthorizationServerConfig
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
authorizationServerConfigurer
.authorizationEndpoint(authorizationEndPoint ->
authorizationEndPoint
.consentPage(CUSTOM_CONSENT_PAGE_URI)
.authorizationRequestConverter(customAuthorizationRequestConverter())
.authorizationResponseHandler(authorizationResponseHandler())
)
.withObjectPostProcessor(new ObjectPostProcessor<OAuth2AuthorizationCodeRequestAuthenticationProvider>() {
#Override
public <O extends OAuth2AuthorizationCodeRequestAuthenticationProvider> O postProcess(O object) {
object.setAuthenticationValidatorResolver(createDefaultAuthenticationValidatorResolver());
return object;
}
});
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequest ->
authorizeRequest.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
return http.build();
}
private AuthenticationConverter customAuthorizationRequestConverter() {
final OAuth2AuthorizationCodeRequestAuthenticationConverter delegate = new OAuth2AuthorizationCodeRequestAuthenticationConverter();
return (request) -> {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
(OAuth2AuthorizationCodeRequestAuthenticationToken) delegate.convert(request);
return authorizationCodeRequestAuthentication;
};
}
// ... validator and authorizationResponseHandler are same as reference
Authentication Provider
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
#RequiredArgsConstructor
#Component
#Slf4j
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final UserMapper userMapper;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
User user = getUser(username);
validatePassword(username, user.getPassword(), password);
return new UsernamePasswordAuthenticationToken(username, password, user.getAuthorities());
}
private User getUser(String username) {
User user = userMapper.fetchUserByUsername(username);
if (user == null)
throw new BusinessException("oauth.user.0100", HttpStatus.NOT_FOUND);
return user;
}
private void validatePassword(String username, String userPassword, String reqPassword) {
if(StringUtils.equals(userPassword, userMapper.fetchEncryptPassword(reqPassword)))
return;
userMapper.updateFailedLoginCount(username);
throw new BusinessException("oauth.user.0100", HttpStatus.NOT_FOUND);
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(
UsernamePasswordAuthenticationToken.class);
}
}
I already have Spring Security Cookie mechanism in place for my application, now only for the API's, I need to add JWT Token-based authentication mechanism. I'm using Spring Security's MultiHttpSecurityConfiguration with two nested class.
Whether both session and JWT token mechanism should be included together in one application or not is a different question altogether, I need to achieve two things.
Spring Security's session-based authentication with cookie will work as it was before.
Need to add an authentication header for API's
package com.leadwinner.sms.config;
import java.util.Collections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import com.leadwinner.sms.CustomAuthenticationSuccessHandler;
import com.leadwinner.sms.CustomLogoutSuccessHandler;
import com.leadwinner.sms.config.jwt.JwtAuthenticationProvider;
import com.leadwinner.sms.config.jwt.JwtAuthenticationTokenFilter;
import com.leadwinner.sms.config.jwt.JwtSuccessHandler;
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
#ComponentScan(basePackages = "com.leadwinner.sms")
public class MultiHttpSecurityConfig {
#Autowired
#Qualifier("userServiceImpl")
private UserDetailsService userServiceImpl;
#Autowired
private JwtAuthenticationProvider authenticationProvider;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceImpl).passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Collections.singletonList(authenticationProvider));
}
#Configuration
#Order(1)
public static class JwtSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationTokenFilter jwtauthFilter;
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.antMatcher("/web/umgmt/**").authorizeRequests()
.antMatchers("/web/umgmt/**").authenticated()
.and()
.addFilterBefore(jwtauthFilter, UsernamePasswordAuthenticationFilter.class);
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
#Configuration
#Order(2)
public static class SecurityConfig extends WebSecurityConfigurerAdapter {
private final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
#Bean
public CustomAuthenticationEntryPoint getBasicAuthEntryPoint() {
return new CustomAuthenticationEntryPoint();
}
#Override
public void configure(HttpSecurity http) throws Exception {
logger.info("http configure");
http
.antMatcher("/**").authorizeRequests()
.antMatchers("/login/authenticate").permitAll()
.antMatchers("/resources/js/**").permitAll()
.antMatchers("/resources/css/**").permitAll()
.antMatchers("/resources/images/**").permitAll()
.antMatchers("/web/initial/setup/**").permitAll()
.antMatchers("/dsinput/**").permitAll().antMatchers("/dsoutput/**").permitAll()
.and()
.formLogin()
.loginPage("/login").usernameParameter("employeeId").passwordParameter("password")
.successForwardUrl("/dashboard")
.defaultSuccessUrl("/dashboard", true)
.successHandler(customAuthenticationSuccessHandler())
.failureForwardUrl("/logout")
.loginProcessingUrl("/j_spring_security_check")
.and().logout()
.logoutSuccessUrl("/logout").logoutUrl("/j_spring_security_logout")
.logoutSuccessHandler(customLogoutSuccessHandler())
.permitAll()
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.and().sessionManagement()
.sessionFixation().none()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.invalidSessionUrl("/logout")
.and().exceptionHandling().accessDeniedPage("/logout").and().csrf().disable();
http.authorizeRequests().anyRequest().authenticated();
}
#Bean
public AuthenticationSuccessHandler customAuthenticationSuccessHandler() {
return new CustomAuthenticationSuccessHandler();
}
#Bean
public LogoutSuccessHandler customLogoutSuccessHandler() {
return new CustomLogoutSuccessHandler();
}
}
}
JwtAuthenticationTokenFilter.java
package com.leadwinner.sms.config.jwt;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String authToken = header.substring(7);
System.out.println(authToken);
try {
String username = jwtTokenUtil.getUsernameFromToken(authToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
if (jwtTokenUtil.validateToken(authToken, username)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
username, null, null);
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
} catch (Exception e) {
System.out.println("Unable to get JWT Token, possibly expired");
}
}
chain.doFilter(request, response);
}
}
JwtTokenUtil.java
package com.leadwinner.sms.config.jwt;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
#Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = 8544329907338151549L;
public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
private String secret = "my-secret";
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, username);
}
private String doGenerateToken(Map<String, Object> claims, String subject) {
return "Bearer "
+ Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
public Boolean validateToken(String token, String usernameFromToken) {
final String username = getUsernameFromToken(token);
return (username.equals(usernameFromToken) && !isTokenExpired(token));
}
}
It seems that now the JwtSecurityConfig filter is not being applied for the path I have mentioned.
Any help will be appreciated.
I have already read this question. I followed the same.
Spring Security with Spring Boot: Mix Basic Authentication with JWT token authentication
Edit: Added JwtAuthenticationTokenFilter, JwtTokenUtil
I got your requirement.
You need to expose API's that should be accessed through JWT token in request header(for each request).
And also web application should be secured through form based authentication mechanism which should work on basis of http session.
You can achieve this by two authentication filters.
Filter - 1: for Rest API (JwtAuthTokenFilter) which should be stateless and identified by Authorization token sent in request each time.
Filter - 2: You need another filter (UsernamePasswordAuthenticationFilter) By default spring-security provides this if you configure it by http.formLogin(). Here each request is identified by the session(JSESSIONID cookie) associated. If request does not contain valid session then it will be redirected to authentication-entry-point (say: login-page).
Recommended URL pattern
api-url-pattern = "/api/**" [strictly for #order(1)]
webApp-url-pattern = "/**" [ wild card "/**" always used for higer order otherwise next order configuration becomes dead configuration]
Approach
Define Main Configuration class with #EnableWebSecurity
Create two inner static classes which should extend WebSecurityConfigurerAdapter and annotated with #Configuration and #Order. Here order for rest api configuration should be 1 and for web application configuration order should be more than 1
Refer my answer in this link for more details which has explaination in depth with necessary code. Feel free to ask for downloadable link from github repository if required.
Limitation
Here both filters will work side by side(Parellally). I mean from web application even though if a user is authenticated by session, he can not access API's without a JWT token.
EDIT
For OP's requirement where he doesn't want to define any role but API access is allowed for authenticated user. For his requirement modified below configuration.
http.csrf().disable()
.antMatcher("/web/umgmt/**").authorizeRequests()
.antMatcher("/web/umgmt/**").authenticated() // use this
I have configured Spring security to work with both LDAP and DB based login. First it tries to login via LDAP and if required permissions is not there then it takes to username/password entry page.
<security:http auto-config="false" entry-point-ref="loginUrlAuthenticationEntryPoint">
<security:custom-filter ref="customPreAuthFilter" position="PRE_AUTH_FILTER"/> // This is for LDAP
<security:custom-filter ref="customAuthFilter" position="FORM_LOGIN_FILTER"/> // This is for DB Based
/** intercept urls
**/
</security:http>
I want to add a new screen on the top and user need to select between the two button LDAP or username/pass. How do I proceed?
The data to be accessed is the same url i.e. /home but both ldap and DB users should be able to access.
If you look at code in UsernamePasswordAuthenticationFilter there is setDetails method.
from docs:
Provided so that subclasses may configure what is put into the
authentication request's details property.
Idea from here
Provision to change ldap/Ad provider url at run time
You can set the details like authtype here and use it authentication provider, But to achieve the things you would lik adds little more work.
Adding details and hope it helps.
CustomUsernamePasswordAuthenticationFilter.java
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
#Component
public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final Logger logger = LoggerFactory.getLogger(CustomUsernamePasswordAuthenticationFilter.class);
#Autowired
#Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
// TODO Auto-generated method stub
super.setAuthenticationManager(authenticationManager);
}
#Autowired
#Override
public void setAuthenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
super.setAuthenticationDetailsSource(authenticationDetailsSource);
}
#Override
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
String authType = request.getParameter("authType");
logger.info("authType {} ",authType);
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
}
But this is not sufficient you would need to extend WebAuthenticationDetails.
Reason is WebAuthenticationDetails provides only remote IP address and sessionId so, to add authType we need to extend this class.
You have to extend WebAuthenticationDetailsSource to return CustomAuthenticationDetails as shown below.
CustomAuthenticationDetails.java
public class CustomAuthenticationDetails extends WebAuthenticationDetails{
private final String authType;
public CustomAuthenticationDetails(HttpServletRequest request) {
super(request);
authType = request.getParameter("authType");
}
public String getAuthType() {
return authType;
}
}
CustomWebAuthenticationDetailsSource.java
public class CustomWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
#Override
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new CustomAuthenticationDetails(context);
}
}
Please note these classes for demo purpose only.
Need to autowire actual authentication providers in these classes.
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.stereotype.Component;
#Component
public class AuthenicationProviderJdbcLdapImpl implements AuthenticationProvider{
// you need to autowire jdbc auth provider
#Autowired(required = false)
DaoAuthenticationProvider authenticationProvider;
//you need to autowire ldap auth provider
#Autowired(required = false)
LdapAuthenticationProvider ldapAuthenticationProvider;
protected static class User{
private final String userId;
private final String password;
public User(String userId,String password) {
this.userId = userId;
this.password = password;
}
public String getUserId() {
return userId;
}
public String getPassword() {
return password;
}
#Override
public String toString() {
return "User [userId=" + userId + ", password=" + password + "]";
}
}
private final static Logger logger = LoggerFactory.getLogger(AuthenicationProviderJdbcLdapImpl.class);
private static final List<User> users1 = Arrays.asList(new User("admin1", "password"),new User("admin2", "password"));
private static final List<User> users2 = Arrays.asList(new User("admin3", "password"),new User("admin4", "password"));
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
CustomAuthenticationDetails details = (CustomAuthenticationDetails) authentication.getDetails();
String authType = details.getAuthType();
logger.info("authType {}",authType);
if("jdbc".equalsIgnoreCase(authType)) {
logger.info("perfrom jdbc authentication");
//perform your authentication using jdbc
//the below is just for explaination
return performAuthentication(authentication, users1);
}else if("ldap".equalsIgnoreCase(authType)) {
logger.info("perfrom ldap authentication");
//perform your authentication using ldap
//the below is just for explaination
return performAuthentication(authentication, users2);
}
return null;
}
private Authentication performAuthentication(Authentication authentication,List<User> users) {
String credential = (String) authentication.getCredentials();
String userId = authentication.getName();
for(User user: users) {
if(user.getUserId().equals(userId)&& user.getPassword().equals(credential)) {
authentication = new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(),authentication.getAuthorities());
return authentication;
}
}
return null;
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
If you would need to redirect different login page (not sure, if you have the requirement) you register AuthenticationFailureHandler shown in security config. Here it is redirected to login and login1 based on condition.
http.failureHandler(new AuthenticationFailureHandler() {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String whichPage = request.getParameter("whichPage");
System.out.println("inside login failure handler "+whichPage);
if("login1".equals(whichPage)) {
response.sendRedirect(request.getContextPath() +"/login1");
}else {
response.sendRedirect(request.getContextPath() +"/login");
}
}
})
WebSecurityConfig.java
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public AuthenticationManager getAuthenticationManager() throws Exception {
return super.authenticationManagerBean();
}
#Autowired
AuthenticationSuccessHandler authenticationSuccessHandler;
#Autowired()
AuthenicationProviderJdbcImpl authenicationProviderJdbcImpl;
#Autowired()
AuthenicationProviderLdapImpl authenicationProviderLdapImpl;
#Autowired
CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAt(customUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http
.authorizeRequests()
.antMatchers("/resources/**", "/registration","/login1").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()//.successHandler(authenticationSuccessHandler)
.failureHandler(new AuthenticationFailureHandler() {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String whichPage = request.getParameter("whichPage");
System.out.println("inside login failure handler "+whichPage);
if("login1".equals(whichPage)) {
response.sendRedirect(request.getContextPath() +"/login1");
}else {
response.sendRedirect(request.getContextPath() +"/login");
}
}
})
.and()
.logout()
.permitAll();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenicationProviderLdapImpl).authenticationProvider(authenicationProviderJdbcImpl);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
/*auth.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder());*/
}
}
The below is from logs when authType = jdbc or authType=ldap
login called
2018-11-23 17:45:25.606 INFO 7232 --- [nio-8080-exec-6] stomUsernamePasswordAuthenticationFilter : authType jdbc
2018-11-23 17:45:25.606 INFO 7232 --- [nio-8080-exec-6] c.t.a.AuthenicationProviderJdbcLdapImpl : authType jdbc
2018-11-23 17:45:25.606 INFO 7232 --- [nio-8080-exec-6] c.t.a.AuthenicationProviderJdbcLdapImpl : perfrom jdbc authentication
login called
login1 called
login1 called
2018-11-23 17:45:42.435 INFO 7232 --- [nio-8080-exec-5] stomUsernamePasswordAuthenticationFilter : authType ldap
2018-11-23 17:45:42.435 INFO 7232 --- [nio-8080-exec-5] c.t.a.AuthenicationProviderJdbcLdapImpl : authType ldap
2018-11-23 17:45:42.435 INFO 7232 --- [nio-8080-exec-5] c.t.a.AuthenicationProviderJdbcLdapImpl : perfrom ldap authentication returning true in ldap
I am working on a Spring Boot 1.4.1 application (that implements some REST web services) and I am finding some difficulties trying to implement Spring Security into this project.
I have the following situation:
1) I have the CustomUserDetails that implements the Spring Security UserDetails interface:
import com.betrivius.domain.User;
import com.betrivius.domain.UserRole;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class CustomUserDetails extends User implements UserDetails {
private static final long serialVersionUID = 1L;
public CustomUserDetails(User user){
super(user);
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
for(UserRole role : this.getUserRoles() ){
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getName());
authorities.add(grantedAuthority);
}
return authorities;
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
#Override
public String getUsername() {
return super.getUsername();
}
}
2) Then I have the CustomUserDetailsService implementing the Spring Security UserDetailsService interface:
import com.betrivius.dao.UserDAO;
import com.betrivius.domain.User;
import com.betrivius.security.bean.CustomUserDetails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
#Transactional
#Service("customUserDetailsService")
public class CustomUserDetailsService implements UserDetailsService{
#Autowired
private UserDAO userDao;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.findByUsername(username);
if(user == null){
throw new UsernameNotFoundException("No user present with username: "+username);
}else{
return new CustomUserDetails(user);
}
}
}
3) Finnally I have the Spring Security confinguration class named WebSecurityConfig extending the WebSecurityConfigurerAdapter:
import com.betrivius.security.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
#Configuration
#EnableWebSecurity
#ComponentScan(basePackageClasses = CustomUserDetailsService.class)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordencoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/Accomodation/**").access("hasRole('ROLE_USER')")
.anyRequest().permitAll()
.and()
.csrf().disable();
/*.and()
.formLogin().loginPage("/login")
.usernameParameter("username").passwordParameter("password")
.and()
.logout().logoutSuccessUrl("/login?logout")
.and()
.exceptionHandling().accessDeniedPage("/403")
.and()
.csrf();*/
}
#Bean(name = "passwordEncoder")
public PasswordEncoder passwordencoder() {
return new BCryptPasswordEncoder();
}
}
As you can see in the previous code snippet in the configuration class I have specified that only a user having the ROLE_USER setted can access to all the resources under the /Accomodation/ path, I have done it by:
.antMatchers("/Accomodation/**").access("hasRole('ROLE_USER')")
4) This is the UserDAO that obtain the user information used into the previous CustomUserDetailsService class:
#Repository
#Transactional(propagation = Propagation.MANDATORY)
public interface UserDAO extends JpaRepository<User, Long> {
User findByUsername(String username);
#Query("SELECT r FROM User u JOIN u.userRoles r where u.username = :username")
List<UserRole> findRoleByUserName(String username);
}
The username and password are stord in the DB and are:
USERNAME: ErSabba
PASSWORD: pswd
User table have a many to many relationship with User_Roles, the role of ErSabba user is ROLE_USER (tried setting USER as role).
So, by PostMan Chrome plugin I perform a GET Request toward the URL:
http://localhost:8080/Accomodation/7
passing the previous credential in this way:
As you can see the problem is that inserting the right username and password into the basic authentication form it give me 403 error Access denied.
I think that it means that I have perform the authentication but I have not the authorization.
The strange thing is that I also tried to put bad credential and I always obtain the same instead of 401 Bad Credential error.
I also have put a breakpoint on this line of the loadUserByUsername() method into the CustomUserDetailsService class (that retrieve the User info).
User user = userDao.findByUsername(username);
Running in debug mode it doesn't stop on this line so it seems that Spring Security configuration doesn't works fine...
What could be the problem? What am I missing? How can I try to fix it?
I would recommend you to do a test without a DB. You can provide an inMemoryconfiguration, like this:
auth.inMemoryAuthentication().withUser("ErSabba").password("pswd").authorities("ROLE_USER");
and try to login then. If it would work, it will show that there is something wrong with credentials in DB.
How do you store password in DB? If it's plain text, it will not work, as you have password encoder, that should read you password as a hash.
Hope that helps.
I'm trying to get my head around spring security for spring boot and all the information I can find online have to do with Spring boot MVC examples where the login.html page is created etc. What I need is a plain old HTTP GET /login page with a username and a password.
What I currently have for my WebSecurityConfig is this:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
#Configuration
#EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
UserDetailsService userDetailsService;
#Autowired
public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordencoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().and()
.authorizeRequests().antMatchers("/login/**").permitAll()
.anyRequest().authenticated().and()
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).deleteCookies().and()
.csrf().disable();
}
#Bean(name="passwordEncoder")
public PasswordEncoder passwordencoder (){
return new BCryptPasswordEncoder();
}
}
And my AuthenticationProviderConfig
#Configuration
public class AuthenticationProviderConfig {
#Autowired
JdbcTemplate jdbcTemplate;
#Autowired
AgentRepository agentRepository;
#Bean(name="userDetailsService")
public UserDetailsService userDetailsService(){
JdbcDaoImpl jdbcImpl = new JdbcDaoImpl();
jdbcImpl.setDataSource(jdbcTemplate.getDataSource());
jdbcImpl.setUsersByUsernameQuery("select username,password, enabled from agent where username=?");
jdbcImpl.setAuthoritiesByUsernameQuery("select b.username, a.role from agent_role a, agent b where b.username=? and a.agent_role_id=b.id");
return jdbcImpl;
}
}
This works fine but it's not ideal for my project.
Now every time a request comes in for any URL on my server the user will be asked for a username and a password through a server realm. In front end app terms that means that the developer needs to provide a basic authentication header with every request they send to the server.
Is there any way to customize the login so that it goes through a POST request like this one:
#Autowired
UserDetailsService userDetailsService;
#RequestMapping(value = "login", method = RequestMethod.POST)
public void login(String username, String password){
userDetailsService.loadUserByUsername(username);
//somehow login and create session?
}
I use the following solution and it works.
Make a controller :
#RequestMapping(value = "/login", method = RequestMethod.POST)
public SecurityUserResource login(#RequestParam("name") String name,#RequestParam("password") String password){
SystemUserJpa userJpa = userService.login(name, password);
return assembler.toResource(userJpa);
}
and in the SystemUserService looks like this :
#Service
#Transactional
public class SystemUserService {
#Autowired SystemUserRepository userRepo;
#Autowired SystemUserDetailsService userService;
#Autowired PasswordEncoder passwordEncoder;
#Autowired ResourceLoader loader;
#Autowired(required=false) SessionService sessionService;
/**
* #param name
* #param password
* #return
*/
public SystemUserJpa login(String name,String password){
// build authentication token for user
SystemUserJpa userJpa = userRepo.findByNameLikeIgnoreCase(name);
if (userJpa==null) {
throw new AuthenticationFailedException("login failed");
}
if (!passwordEncoder.matches(password, userJpa.getEncryptedPassword())) {
throw new AuthenticationFailedException("login failed");
}
UserDetails user = userService.loadUserByUsername(name);
final Authentication auth = new UsernamePasswordAuthenticationToken(name, password,user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
if (sessionService!=null) {
sessionService.login(userJpa);
}
return userJpa;
}
}
The important is
SecurityContextHolder.getContext().setAuthentication(auth);
After this, the user is authenticated and this is sored in the session. When the next request from that user arrives, the same session is used, and because its authenticated he can acces pages thaat require authentication without getting asked for a passwordd again.