I am currently working on my own project which should have its own oauth authentication server using spring-boot and spring-oauth.
I get
TokenEndpoint : Handling error: InvalidGrantException, Bad credentials even the name and password are right.
I save my users in mysql and encode the passwords with bCrypt.
Below are my configurations
I tried:
{noop} in password
tried different implementations of the passwordEncoder Bean
tried to use a Custom UserDetailsServiceImplementation
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private PasswordEncoder passwordEncoder;
/**
* Setting up the endpointsconfigurer authentication manager.
* The AuthorizationServerEndpointsConfigurer defines the authorization and token endpoints and the token services.
* #param endpoints
* #throws Exception
*/
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
endpoints.tokenStore(getTokenStore());
}
#Bean
public TokenStore getTokenStore(){
return new InMemoryTokenStore();
}
/**
* Setting up the clients with a clientId, a clientSecret, a scope, the grant types and the authorities.
* #param clients
* #throws Exception
*/
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("my-trusted-client")
.authorizedGrantTypes("password")
.authorities("ROLE_USER").scopes("read","write","trust")
.resourceIds("oauth2-resource").accessTokenValiditySeconds(5000).secret(passwordEncoder.encode("secret"));
}
/**
* We here defines the security constraints on the token endpoint.
* We set it up to isAuthenticated, which returns true if the user is not anonymous
* #param security the AuthorizationServerSecurityConfigurer.
* #throws Exception
*/
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("isAuthenticated()");
}
}
public class CustomUserDetails implements UserDetails {
private String password;
private String username;
private Collection<? extends GrantedAuthority> authorities;
public CustomUserDetails(User user) {
this.username = user.getUsername();
this.password = user.getPassword();
this.authorities = translate(user.getRole());
}
private Collection<? extends GrantedAuthority> translate(Role role) {
List<GrantedAuthority> authorities = new ArrayList<>();
String roleName = role.getRole().toUpperCase();
authorities.add(new SimpleGrantedAuthority(roleName));
return authorities;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
#Override
public String getPassword() {
return password;
}
#Override
public String getUsername() {
return username;
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
}
#Configuration
#EnableResourceServer
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/","/home","/register","/login").permitAll()
.antMatchers("/private/**").authenticated()
.antMatchers("/post").authenticated()
.antMatchers("/post/postComment").authenticated()
.antMatchers(HttpMethod.DELETE , "/post/**").hasAuthority("ROLE_ADMIN");
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
#SpringBootApplication
#EnableAuthorizationServer
public class BackendApplication {
#Autowired
private PasswordEncoder passwordEncoder;
public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}
#Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* Password grants are switched on by injecting an AuthenticationManager.
* Here, we setup the builder so that the userDetailsService is the one we coded.
* #param builder
* #param repository
* #throws Exception
*/
#Autowired
public void authenticationManager(AuthenticationManagerBuilder builder, UserRepository repository, UserService userService) throws Exception {
if (repository.count()==0) {
userService.save(new User("admin", "{noop}adminPassword", new Role("ROLE_USER")));
}
builder.userDetailsService(userDetailsService(repository)).passwordEncoder(passwordEncoder);
}
/**
* We return an istance of our CustomUserDetails.
* #param repository
* #return
*/
private UserDetailsService userDetailsService(final UserRepository repository) {
return username -> new CustomUserDetails(repository.findByUsername(username));
}
}
Warning Message:
2019-07-24 15:46:42.341 WARN 73936 --- [nio-8088-exec-4] o.s.s.o.provider.endpoint.TokenEndpoint : Handling error: InvalidGrantException, Bad credentials
URL: http://localhost:8088/oauth/token
The Request is url-www-form-encoded with the Parameters:
grant_type:password
username:admin
password:adminPassword
and "Basic Auth" with Username "my-trusted-client" and Password "secret".
Since the request isn't provided I have to guess: You shouldn't encode the client secret when configured with ClientDetailsServiceConfigurer.
Replace .secret(passwordEncoder.encode("secret")) with .secret("secret") and the Exception should vanish.
Use .secret("{bcrypt}" + passwordEncoder.encode("secret")) for my-trusted-client and .secret("{bcrypt}" + passwordEncoder.encode("adminPassword")) for admin. And: Check if the BcryptPasswordEncoder is used in a debugging session.
I was getting this error, found I was returning UserDetails with plain password. so added something like passwordEncoder.encode(password) while setting password (I wasn't getting it from db call) in the returning UserDetails.
Related
I am trying to implement oauth2 with a jwt in spring boot and the autentication works but when I want to get the refresh_token an error occurs that indicates the following ...
java.lang.IllegalStateException: UserDetailsService is required.
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$UserDetailsServiceDelegator.loadUserByUsername(WebSecurityConfigurerAdapter.java:464)
at org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper.loadUserDetails(UserDetailsByNameServiceWrapper.java:68)
at org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider.authenticate(PreAuthenticatedAuthenticationProvider.java:103)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:175)
at org.springframework.security.oauth2.provider.token.DefaultTokenServices.refreshAccessToken(DefaultTokenServices.java:150)
What am I doing wrong?
These are my files
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Value("${token.secret}")
private String secret;
#Value("${server.servlet.context-path}")
private String contextPath;
#Value("${oauth2.client.id}")
public String CLIENT_ID;
#Value("${oauth2.client.secret}")
public String CLIENT_SECRET;
#Value("${oauth2.scope.read}")
public String SCOPE_READ;
#Value("${oauth2.grant.types}")
public String GRANT_TYPES;
#Value("${oauth2.scopes}")
public String SCOPES;
#Value("${oauth2.access.token.validity}")
public Integer TOKEN_VALID_SECONDS;
#Value("${oauth2.refresh.token.validity}")
public Integer REFRESH_TOKEN_VALID_SECONDS;
#Autowired
private DataSource dataSource;
#Autowired
#Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
#Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(CLIENT_ID)
.secret(passwordEncoder().encode(CLIENT_SECRET))
.authorizedGrantTypes("password", "refresh_token", "client_credentials","authorization_code")
.scopes("read", "write", "trust")
.accessTokenValiditySeconds(TOKEN_VALID_SECONDS)
.refreshTokenValiditySeconds(REFRESH_TOKEN_VALID_SECONDS);
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
#Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer()));
endpoints.tokenStore(tokenStore()).tokenEnhancer(tokenEnhancerChain).authenticationManager(authenticationManager);
}
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
And this is my SecurityConfig class
#EnableGlobalMethodSecurity(securedEnabled=true)
#Configuration
#RequiredArgsConstructor
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private final CustomUserDetailsService customUserDetailsService;
#Autowired
public BCryptPasswordEncoder passwordEncoder;
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.antMatchers("/oauth/token/**").permitAll()
.antMatchers("/api/**" ).authenticated()
.anyRequest().authenticated()
.and().formLogin().permitAll()
.and().csrf().disable();
}
}
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
private UserService userService;
//login web service url describe on properties file
#Value("${loginServiceUri}")
public String loginServiceUri;
//login web service enabled
#Value("${loginWebServiceEnabled}")
public Boolean loginWebServiceEnabled;
#Override
public UserDetails loadUserByUsername(String username) {
log.debug("Trying to authenticate user with details....");
if (loginWebServiceEnabled && loginServiceUri != null){
//its necessary to call external Web Service to find the user and then look for
//into database
UserDto userDto = this.loginWebService(username);
if (userDto != null) {
// look for the user in data base
log.debug("User found at external login web service trying to look for at data base");
return lookUserDataBase(userDto.getUsername());
} else {
log.error("User not found at external login web service", username);
throw new UsernameNotFoundException(username);
}
} else {
// look for the user in data base
return lookUserDataBase(username);
}
}
/**
* Look for use in data base by user name
* #return
*/
private UserDetails lookUserDataBase(String userName) {
UserEntity user = userService.findEntityByUsername(userName);
if (user == null) {
log.error("User not found in data base", userName);
throw new UsernameNotFoundException(userName);
}
log.debug("User found in data base with userName: " + userName + " and authorities: " + user.getUserAuthority().toString());
return new UserAuthenticatedDetails(user);
}
/**
* Example Login Web Service call login
* #param name
* #return
*/
private UserDto loginWebService (String name){
xxxxxxxxxxx
}
}
Add these in your WebSecurityConfigurerAdapter class
#Autowired
public void setApplicationContext(ApplicationContext context) {
super.setApplicationContext(context);
AuthenticationManagerBuilder globalAuthBuilder = context
.getBean(AuthenticationManagerBuilder.class);
try {
globalAuthBuilder.userDetailsService(userDetailsService);
} catch (Exception e) {
e.printStackTrace();
}
}
There is a "local" AuthenticationManagerBuilder and a "global" AuthenticationManagerBuilder and we need to set it on the global version in order to have this information passed to these other builder contexts.
Read more about it here
I solved with this lines in SecurityConfig;
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer()));
endpoints.tokenStore(tokenStore()).tokenEnhancer(tokenEnhancerChain).authenticationManager(authenticationManager);
//this line solved the problem
endpoints.userDetailsService(customUserDetailsService);**
}
Either you configure some inMemory user as follows:
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password("password")
.roles("USER")
.and()
.withUser("manager")
.password("password")
.credentialsExpired(true)
.accountExpired(true)
.accountLocked(true)
.authorities("WRITE_PRIVILEGES", "READ_PRIVILEGES")
.roles("MANAGER");
}
or you implement a UserDetailService e.g. which looks up in the database for a user.
I am learning how to use Spring and how to implement Spring Security in my project, using Roles and MongoDB.
I have a User model with a role list, and I want to let only ADMIN user to use some endpints from Controller.
Here is the User class and Role enum:
#Document(collection = "Users")
public class User {
#Id
private String id;
private String firstName;
private String lastName;
private String email;
private String password;
private List<Role> roles;
//constructor + getters and setters
}
public enum Role {
ADMIN,
BASIC_USER
}
I use the this UserDetails implementation: I think here is the problem...
public class CustomUserDetails implements UserDetails {
private User user;
public CustomUserDetails() {
}
public CustomUserDetails(User user) {
this.user = user;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.user.getRoles().stream().map(role -> new SimpleGrantedAuthority(String.format("ROLE_%s", role))).collect(Collectors.toList());
}
#Override
public String getPassword() {
return this.user.getPassword();
}
#Override
public String getUsername() {
return this.user.getEmail();
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
public User getUser() {
return user;
}
}
The UserDetailsService looks like this:
public class CustomUserDetailsServiceImpl implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
final User user = userRepository.findByEmail(email);
if (Objects.isNull(user)) {
throw new UsernameNotFoundException("User not found");
}
return new CustomUserDetails(user);
}
}
I made this configuration:
#Configuration
#EnableWebSecurity(debug = true)
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(createUserDetailsService()).passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/users/**").permitAll()
.antMatchers( "/users/**").authenticated().anyRequest().hasAnyRole("ADMIN")
.antMatchers("/users/me").authenticated().anyRequest().hasAnyRole("ADMIN", "BASIC_USER")
.antMatchers( "/users/**").permitAll()
.and()
.formLogin().disable()
.httpBasic();
}
#Override
public void configure(WebSecurity web) throws Exception {
/* To allow Pre-flight [OPTIONS] request from browser */
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
}
/**
* Create password encoder bean used for encrypting the password
*
* #return
*/
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Create user service bean used for find the user by email
* #return
*/
#Bean
public UserDetailsService createUserDetailsService() {
return new CustomUserDetailsServiceImpl();
}
}
When I make any call with Postman at localhost:8080/users using Basic auth with the admin details from DB, all the time I get 401 Unauthorized Status
I think the problem is that I use an Enum for Roles and I don't know how to correctly build UserDetails implementation.
If helps, this is the UserController
#RestController
#RequestMapping("/users")
public class UserController {
#Autowired
private UserService userService;
#GetMapping(path = "")
public List<User> getUsers(){
return this.userService.getUsers();
}
}
When I used MySQL for this project (With the same classes for security) the apps worked perfectly. In that implementation I used Roles, Users and Users_Roles tables.
I noticed that my ResponseEntityExceptionHandler does not work with exceptions thrown by Spring Security in my Spring Boot application.
However, I need a way to catch InvalidGrantException which seems to get thrown when a user account is still disabled.
The use case is simple: If a user is currently disabled, I want to throw an appropriate error to my client s.t. it can display a message accordingly.
Right now the response by Spring Security is
{
error: "invalid_grant",
error_description: "User is disabled"
}
I saw this question but for some reason my AuthFailureHandler is not getting invoked:
#Configuration
#EnableResourceServer
public class ResourceServer extends ResourceServerConfigurerAdapter {
#Component
public class AuthFailureHandler implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// Never reached ..
System.out.println("Hello World!");
}
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(customAuthEntryPoint());;
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationEntryPoint(customAuthEntryPoint());
}
#Bean
public AuthenticationEntryPoint customAuthEntryPoint(){
return new AuthFailureHandler();
}
}
Any idea what I am missing?
Configuration Code
Here is my OAuth2Configuration which also contains a ResourceServerConfiguraerAdapter which is supposed to handle the exception
#Configuration
#EnableAuthorizationServer
#EnableResourceServer
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {
private final TokenStore tokenStore;
private final JwtAccessTokenConverter accessTokenConverter;
private final AuthenticationManager authenticationManager;
#Autowired
public OAuth2Configuration(TokenStore tokenStore, JwtAccessTokenConverter accessTokenConverter, AuthenticationManager authenticationManager) {
this.tokenStore = tokenStore;
this.accessTokenConverter = accessTokenConverter;
this.authenticationManager = authenticationManager;
}
#Value("${security.jwt.client-id}")
private String clientId;
#Value("${security.jwt.client-secret}")
private String clientSecret;
#Value("${security.jwt.scope-read}")
private String scopeRead;
#Value("${security.jwt.scope-write}")
private String scopeWrite;
#Value("${security.jwt.resource-ids}")
private String resourceIds;
private final static String WEBHOOK_ENDPOINTS = "/r/api/*/webhooks/**";
private final static String PUBLIC_ENDPOINTS = "/r/api/*/public/**";
#Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
configurer
.inMemory()
.withClient(clientId)
.secret(clientSecret)
.scopes(scopeRead, scopeWrite)
.resourceIds(resourceIds)
.accessTokenValiditySeconds(60*60*24*7 * 2) // Access tokens last two weeks
.refreshTokenValiditySeconds(60*60*24*7 * 12) // Refresh tokens last 12 weeks
//.accessTokenValiditySeconds(5)
//.refreshTokenValiditySeconds(10)
.authorizedGrantTypes("password", "refresh_token"); //, "client_credentials");
}
/**
* Since there are currently multiply clients, we map the OAuth2 endpoints under /api
* to avoid conflicts with #{#link ForwardController}
*/
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Collections.singletonList(accessTokenConverter));
endpoints.tokenStore(tokenStore)
// Create path mappings to avoid conflicts with forwarding controller
.pathMapping("/oauth/authorize", "/api/v1/oauth/authorize")
.pathMapping("/oauth/check_token", "/api/v1/oauth/check_token")
.pathMapping("/oauth/confirm_access", "/api/v1/oauth/confirm_access")
.pathMapping("/oauth/error", "/api/v1/oauth/error")
.pathMapping("/oauth/token", "/api/v1/oauth/token")
.accessTokenConverter(accessTokenConverter)
.tokenEnhancer(enhancerChain)
.reuseRefreshTokens(false)
.authenticationManager(authenticationManager);
}
/**
* Forwarding controller.
*
* This controller manages forwarding in particular for the static web clients. Since there are multiple
* clients, this controller will map <i>any</i> GET request to the root /* to one of the clients.
*
* If no match was found, the default redirect goes to /web/index.html
*
*/
#Controller
public class ForwardController {
#RequestMapping(value = "/sitemap.xml", method = RequestMethod.GET)
public String redirectSitemapXml(HttpServletRequest request) {
return "forward:/a/web/assets/sitemap.xml";
}
#RequestMapping(value = "/robots.txt", method = RequestMethod.GET)
public String redirectRobotTxt(HttpServletRequest request) {
return "forward:/a/web/assets/robots.txt";
}
#RequestMapping(value = "/*", method = RequestMethod.GET)
public String redirectRoot(HttpServletRequest request) {
return "forward:/a/web/index.html";
}
#RequestMapping(value = "/a/**/{path:[^.]*}", method = RequestMethod.GET)
public String redirectClients(HttpServletRequest request) {
String requestURI = request.getRequestURI();
if (requestURI.startsWith("/a/admin/")) {
return "forward:/a/admin/index.html";
}
if (requestURI.startsWith("/a/swagger/")) {
return "forward:/a/swagger/swagger-ui.html#/";
}
return "forward:/a/web/index.html";
}
}
#Configuration
#EnableResourceServer
public class ResourceServer extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.requiresChannel()
/* Require HTTPS evereywhere*/
.antMatchers("/**")
.requiresSecure()
.and()
.exceptionHandling()
.and()
/* Permit all requests towards the public api as well as webhook endpoints. */
.authorizeRequests()
.antMatchers(PUBLIC_ENDPOINTS, WEBHOOK_ENDPOINTS)
.permitAll()
/* Required for ForwardController */
.antMatchers(HttpMethod.GET, "/*")
.permitAll()
.antMatchers("/r/api/**")
.authenticated();
// #formatter:on
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.resourceId(resourceIds)
.authenticationEntryPoint(customAuthEntryPoint());
}
#Component
public class AuthFailureHandler implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
System.out.println("Hello");
// FIXME We need to return HTTP 401 (s.t. the client knows what's going on) but for some reason it's not working as intended!
throw authException;
}
}
#Bean
public AuthenticationEntryPoint customAuthEntryPoint(){
return new AuthFailureHandler();
}
}
}
In addition, here is the WebSecurityConfigurerAdapter although I do not think this plays a role here:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${security.signing-key}")
private String signingKey;
private final UserDetailsService userDetailsService;
private final DataSource dataSource;
#Autowired
public WebSecurityConfig(#Qualifier("appUserDetailsService") UserDetailsService userDetailsService, DataSource dataSource) {
this.userDetailsService = userDetailsService;
this.dataSource = dataSource;
}
#Bean
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
#Override
public void configure(WebSecurity web) throws Exception {
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(signingKey);
return converter;
}
/**
* Using {#link JwtTokenStore} for JWT access tokens.
* #return
*/
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
/**
* Provide {#link DefaultTokenServices} using the {#link JwtTokenStore}.
* #return
*/
#Bean
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
/**
* We provide the AuthenticationManagerBuilder using our {#link UserDetailsService} and the {#link BCryptPasswordEncoder}.
* #param auth
* #throws Exception
*/
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(this.userDetailsService)
.passwordEncoder(passwordEncoder())
.and()
.authenticationProvider(daoAuthenticationProvider());
}
/**
* Using {#link BCryptPasswordEncoder} for user-password encryption.
* #return
*/
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Provide {#link DaoAuthenticationProvider} for password encoding and set the {#link UserDetailsService}.
* #return
*/
#Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsService(this.userDetailsService);
return daoAuthenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.requiresChannel()
.requestMatchers(r -> r.getHeader("X-Forwarded-Proto") != null)
.requiresSecure();
}
}
Create a custom class that extends ResponseEntityExceptionHandler, annotate it with #ControllerAdvice and handle OAuth2Exception (base class of InvalidGrantException).
#ExceptionHandler({OAuth2Exception.class})
public ResponseEntity<Object> handleOAuth2Exception(OAuth2Exception exception, WebRequest request) {
LOGGER.debug("OAuth failed on request processing", exception);
return this.handleExceptionInternal(exception, ErrorOutputDto.create(exception.getOAuth2ErrorCode(), exception.getMessage()), new HttpHeaders(), HttpStatus.valueOf(exception.getHttpErrorCode()), request);
}
Use HandlerExceptionResolverComposite to composite all exception resolvers in the system into one exception resolver. This overrides the corresponding bean defined in
WebMvcConfigurationSupport class. Add list of exceptionResolvers, like DefaultErrorAttributes and ExceptionHandlerExceptionResolver (this enables AOP based exceptions handling by involving classes with #ControllerAdvice annotation such as custom class that we created that extends ResponseEntityExceptionHandler.
<bean id="handlerExceptionResolver" class="org.springframework.web.servlet.handler.HandlerExceptionResolverComposite">
<property name="exceptionResolvers">
<list>
<bean class="org.springframework.boot.web.servlet.error.DefaultErrorAttributes"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver">
<property name="messageConverters">
<list>
<ref bean="jackson2HttpMessageConverter" />
</list>
</property>
</bean>
</list>
</property>
Have you tried if defining a #ControllerAdvice specifying your InvalidGrantException works?
#ControllerAdvice
#ResponseBody
public class GlobalExceptionHandler {
#ExceptionHandler(InvalidGrantException.class)
public ResponseEntity<CustomErrorMessageTO> handleInvalidGrant(
InvalidGrantException invalidGrantException) {
CustomErrorMessageTO customErrorMessageTO = new CustomErrorMessageTO("Not granted or whatsoever");
return new ResponseEntity<>(customErrorMessageTO, HttpStatus.UNAUTHORIZED);
}
}
class CustomErrorMessageTO {
private String message;
public CustomErrorMessageTO(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
I want to login as a user but for some reason it seems that the endpoint oauth/token is protected:
Request URL:http://192.168.0.14:8080/oauth/token
Request Method:POST
Status Code:401
Remote Address:192.168.0.14:8080
Referrer Policy:no-referrer-when-downgrade
Access-Control-Allow-Headers:x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN
Access-Control-Allow-Methods:PATCH,POST,GET,OPTIONS,DELETE
Access-Control-Allow-Origin:*
Access-Control-Max-Age:3600
Cache-Control:no-store
Cache-Control:no-cache, no-store, max-age=0, must-revalidate
Content-Type:application/json;charset=UTF-8
Date:Tue, 06 Mar 2018 18:59:25 GMT
Expires:0
Pragma:no-cache
Pragma:no-cache
Transfer-Encoding:chunked
WWW-Authenticate:Bearer realm="testjwtresourceid", error="unauthorized", error_description="Full authentication is required to access this resource"
WWW-Authenticate:Basic realm="oauth2/client"
X-Content-Type-Options:nosniff
X-Frame-Options:DENY
X-XSS-Protection:1; mode=block
Interestingly enough... I get this message:
2018-03-06 19:59:25.766 WARN 31030 --- [nio-8080-exec-2] o.s.s.c.bcrypt.BCryptPasswordEncoder : Encoded password does not look like BCrypt
It seems that for some reason the whole thing runs through the BasicAuthenticationFilter. It's clear that this does not look like BCrypt .. though I am setting BCrypt a s my password encoder:
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
auth.authenticationProvider(daoAuthenticationProvider());
}
/**
* Using {#link BCryptPasswordEncoder} for user-password encryption.
* #return
*/
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
It looks like WebSecurity tries to encode the client secret with BCrypt O_o
security.jwt.client-id=CLIENT_ID
security.jwt.client-secret=CLIENT_SECRET
The entire web config
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${security.signing-key}")
private String signingKey;
#Value("${security.encoding-strength}")
private Integer encodingStrength;
#Value("${security.security-realm}")
private String securityRealm;
#Autowired
private UserDetailsService userDetailsService;
#Bean
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
/**
* Nothing to configure yet.
* #param http
* #throws Exception
*/
#Override
protected void configure(HttpSecurity http) throws Exception { }
/**
* Define routes for {#link WebSecurity}.
*
* #param web
* #throws Exception
*/
#Override
public void configure(WebSecurity web) throws Exception {
final String[] SWAGGER_UI = {
"/swagger-resources/**",
"/swagger-ui.html",
"/v2/api-docs",
"/webjars/**"
};
web.ignoring().antMatchers("/pub/**", "/users")
.antMatchers(SWAGGER_UI);
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(signingKey);
return converter;
}
/**
* Using {#link JwtTokenStore} for JWT access tokens.
* #return
*/
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
/**
* Provide {#link DefaultTokenServices} using the {#link JwtTokenStore}.
* #return
*/
#Bean
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
/**
* We provide the AuthenticationManagerBuilder using our {#link UserDetailsService} and the {#link BCryptPasswordEncoder}.
* #param auth
* #throws Exception
*/
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()).and()
.authenticationProvider(daoAuthenticationProvider());
}
/**
* Using {#link BCryptPasswordEncoder} for user-password encryption.
* #return
*/
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Provide {#link DaoAuthenticationProvider} for password encoding and set the {#link UserDetailsService}.
* #return
*/
#Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsService(this.userDetailsService);
return daoAuthenticationProvider;
}
}
The OAuth config
#Configuration
#EnableAuthorizationServer
#EnableResourceServer
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {
#Configuration
#EnableResourceServer
public class ResourceServer extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/**")
.and()
.authorizeRequests().anyRequest().access("#oauth2.hasScope('write')");
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(resourceIds);
}
}
#Value("${security.jwt.client-id}")
private String clientId;
#Value("${security.jwt.client-secret}")
private String clientSecret;
#Value("${security.jwt.grant-type}")
private String grantType;
#Value("${security.jwt.scope-read}")
private String scopeRead;
#Value("${security.jwt.scope-write}")
private String scopeWrite;
#Value("${security.jwt.resource-ids}")
private String resourceIds;
#Autowired
private TokenStore tokenStore;
#Autowired
private JwtAccessTokenConverter accessTokenConverter;
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
configurer
.inMemory()
.withClient(clientId)
.secret(clientSecret)
.authorizedGrantTypes(grantType)
.scopes(scopeRead, scopeWrite)
.resourceIds(resourceIds);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
endpoints.tokenStore(tokenStore)
.accessTokenConverter(accessTokenConverter)
.tokenEnhancer(enhancerChain)
.authenticationManager(authenticationManager);
}
}
Because you have the following Bean created, all security configurations will use this PasswordEncoder. In your case, WebSecurity and AuthorizationServerSecurity.
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
I am pretty sure your security.jwt.client-secret is a plain text.
So, what is happening is that when you make the request, your AuthorizationSecurityConfig tried to read security.jwt.client-secret as a BCrypt encoded string, which eventually it is not.
Hence, the whole Authentication process fails and the warning message is logged.
To resolve this, you have two options:
First:
Configure the AuthorizationSecurity to use NoOpPasswordEncoder. Here is how you can override this. Mind you NoOpPasswordEncoder is already deprecated.
#Override
public void configure(final AuthorizationServerSecurityConfigurer security) throws Exception {
security.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
Second (Preferred):
Generate the BCrypt value of your security.jwt.client-secret and set it in your application.properties/yaml.
Thanks to David who cleared the doubt on a similar answer here.
Guess what, I was facing the same issue when I was looking at this question for solution. :-)
I have implemented the security layer in my project using Spring Boot. Now,I would like to know how to get the request parameters using HttpServletRequest in #Service or #Dao layer. I tried some way to get the request parameters and I could the username and password but I need to pass that in the Dao. My code:
Security Layer code:
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class ApplicationSecurity extends WebSecurityConfigurerAdapter{
#Autowired
UserDao userDao;
#Autowired
HttpServletRequest request;
#Autowired
#Qualifier("userDetailsService")
UserDetailsService userDetailsService;
UserDetails userDetails;
#Autowired
private RESTAuthenticationEntryPoint authenticationEntryPoint;
#Autowired
private RESTAuthenticationFailureHandler authenticationFailureHandler;
#Autowired
private RESTAuthenticationSuccessHandler authenticationSuccessHandler;
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/fonts/**", "/images/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
request.getParameter("username");
http.authorizeRequests().antMatchers("/", "/index.html","/home.html","/static/*","/home/*", "/login.html","/login").permitAll();
http.authorizeRequests().anyRequest().fullyAuthenticated().and().httpBasic().and().csrf().disable();
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http.formLogin().usernameParameter("username").passwordParameter("password").loginProcessingUrl("/login/authenticate").successHandler(authenticationSuccessHandler);
http.formLogin().failureHandler(authenticationFailureHandler);
http.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).invalidateHttpSession(true);
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
// CSRF tokens handling
http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class);
http.addFilterBefore(tokenProcessingFilter(), RequestFetcher.class);
}
/**
* Configures the authentication manager bean which processes authentication
* requests.
*/
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// Dao based authentication
auth.userDetailsService(userDetailsService).passwordEncoder(new Md5PasswordEncoder());
}
private AccessDeniedHandler accessDeniedHandler() {
return new AccessDeniedHandler() {
#Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.getWriter().append("Access denied");
response.setStatus(403);
}
};
}
/**
* This is used to hash the password of the user
* when we need to use BCrypt.
*/
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
/**
* This bean is load the user specific data when form login is used.
*/
#Bean
public UserDetailsService userDetailsService() {
return new MyCustomUserDetailsService(userDao);
}
#Bean
public RequestFetcher tokenProcessingFilter() throws Exception {
RequestFetcher tokenProcessingFilter = new RequestFetcher();
tokenProcessingFilter.setAuthenticationManager(authenticationManager());
return tokenProcessingFilter;
}
}
public class RequestFetcher extends UsernamePasswordAuthenticationFilter{
private String userName = "";
private String password = "";
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = this.getAsHttpRequest(request);
userName = httpRequest.getParameter("username");
System.out.println("===Username===" +userName);
password = httpRequest.getParameter("password");
System.out.println("===Password===" +password);
chain.doFilter(request, response);
}
private HttpServletRequest getAsHttpRequest(ServletRequest request){
if (!(request instanceof HttpServletRequest)) {
throw new RuntimeException("Expecting an HTTP request");
}
return (HttpServletRequest) request;
}
public String getUserName(){
return userName;
}
public void setUserName(String userName){
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Could anybody please help me to get the request parameters?