We're developing Spring 4 REST/JSON API but we need to have custom Authentication service to authenticate against 3rd party service.
restrictions: We don't want to ask the user for username/password. we are authenticating using "cookie" (send with the request initiator). And we need this authentication process in background. (Might sound weird but that's the case).
we could implement that using registering custom authentication/authorization request filters. but that made us loosing the power of spring "authorization" module that we are planning to use afterwards.
So what we did till now, wrote custom WebSecurityConfigurerAdapter with our own custom AuthenticationProvider and UserDetailsService but these configuration doesn't seem to work.
the application doesn't go inside AuthenticationProvider.authenticate
here is the configuration we had.
AuthenticationProvider.java:
#Service
public class AuthenticationService implements AuthenticationProvider, UserDetailsService {
#Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
// DOESN'T ENTER THIS FUNCTION
// do authentication stuff
}
#Override
public boolean supports(Class<?> authentication) {
// JUST FOR TESTING
return true;
}
#Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
// DOESN'T ENTER THIS METHOD
return null;
}
}
SecurityConfig.java:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(SecurityConfig.class);
#Autowired
private AuthenticationService authService;
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/ignoredURL/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //HTTP with Disable CSRF
.authorizeRequests()
.antMatchers("/api/XYZ/**").hasRole("ADMIN")
.anyRequest().authenticated();
// adding ".httpBasic()" automatically prompts user for username/password
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// THIS IS NOT TYPO, I use one service implements both interfaces.
auth.userDetailsService(authService);
auth.authenticationProvider(authService);
}
}
Fixed by adding 2 more classes (Filter extending AbstractPreAuthenticatedProcessingFilter and another class CustomUser implements UserDetails) then made my AuthenticaitonService implements spring UserDetailsService here are the details:
please have a look on what this filter does and how it works
1- Create AbcFilter extends spring AbstractPreAuthenticatedProcessingFilter
2- Override "getPreAuthenticatedPrincipal". This method extracted the cookie value and return it. (Whatever the Object you return here is what you get as a parameter in UserDetailsService.loadUserByUsername).
3- Modified my service to implement spring UserDetailsServiceand inside loadUserByUsername did all authentication logic here and set all logged in user in CustomUser object. Then return it.
4- Whenever request matches /api/XYZ/**, Spring will call your CustomUser.getAuthorities and try to find ADMIN role there.
Related
I'd like to override spring's default AuthorizationEndpoint and provide my own on /oauth/authorize. I wrote my own controller
#RestController
#RequestMapping("oauth/authorize")
public class AuthorizationController {
#RequestMapping
public void authorize(#RequestParam Map<String, String> parameters, HttpServletResponse response) throws Exception {
// todo
}
}
However it is not mapped as AuthorizationEndpoint maps to /oauth/authorize by default. How can I remove the standard implementation?
Bonus
The reason I want to provide my own implementation is because my rest api is stateless and does not provide sessions and/or web interface, standalone angular app does that for me and authorizes using passwrd grant to server. So what I want to do is redirect the user to my angular app's approoval page and implement a custom user_oauth_approval approveOrDeny endpoint which my client calls. I'm not sure if I can set that up with spring, and even if I could, custom implementation would probably be less hassle. I'd love to hear some insights
Inject your customAuthenticationManager in your new controller.
#Autowired
private AuthenticationManager authenticationManager;
#RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
public ResponseEntity<?> createAuthenticationToken(#RequestBody JwtAuthenticationRequest authenticationRequest)
throws AuthenticationException {
Authentication customAuthentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
authenticationRequest.getUsername(), authenticationRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
return ResponseEntity
.ok(new JwtAuthenticationResponse(customAuthentication.getToken(), customAuthentication.isActive()));
}
Then overwrite default Spring AuthenticationManager + AuthenticationProvider
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
#Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) {
authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider);
}
#Bean(name = BeanIds.AUTHENTICATION_MANAGER)
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
import org.springframework.security.authentication.AuthenticationProvider;
#Component("customAuthenticationProvider")
public class CustomAuthenticationProvider implements AuthenticationProvider {
I am implementing a REST API using Spring Boot (2.0.1) to work with MongoDB (3.6). I'm really stuck. I've also tried other tips from StackOverFlow but it didn't help for some reason.
I have configured the SecurityConfig.java to permit the access to certain areas and also created a User inMemoryAuthentication, to be able to login to HAL Browser (Spring) and etc. But the problem is, that whatever address I put in browser I get a Login form and the credentials used in the inMemoryAuthentication is always wrong for some reason. The only way I've found to access the API is by excluding SecurityAutoConfiguration in the main class. But this opens up every permission to access everything including HAL Browser without authentication.
Would someone show me what I am doing wrong? I want to permit only certain paths/addresses to everyone, permit everything else only to use with TokenAuthentication (have already a custom implementation of it) and have one user (username, password) to access HAL Browser.
Here is my SecurityConfig.java:
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserService userService;
#Autowired
private final TokenAuthenticationService tokenAuthenticationService;
#Autowired
protected SecurityConfig(final TokenAuthenticationService tokenAuthenticationService) {
super();
this.tokenAuthenticationService = tokenAuthenticationService;
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.csrf().disable()
.addFilterBefore(new TokenAuthenticationFilter(tokenAuthenticationService), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/hello").permitAll()
.antMatchers("/test2").permitAll()
.antMatchers("/register").permitAll()
.antMatchers("/useraccount").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().permitAll()
.and()
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).permitAll();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// auth
// .inMemoryAuthentication()
// .withUser("user1").password("password").roles("ADMIN");
auth
.inMemoryAuthentication()
.withUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER"));
// auth
// .userDetailsService(userService);
}
// #Bean
// #Override
// public UserDetailsService userDetailsService() {
// UserDetails user =
// User.withDefaultPasswordEncoder()
// .username("user")
// .password("password")
// .roles("USER")
// .build();
//
// return new InMemoryUserDetailsManager(user);
// }
// #Bean
// public UserDetailsService userDetailsService() {
// InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// manager.createUser(User.withUsername("user").password("pass").roles("USER", "ADMIN").build());
// return manager;
// }
}
I've tried different approaches as you see (commented blocks) but still no luck.
Even though I have permitAll() on /register, i still get the auto generated login form, which won't accept any credentials.
So as i've said earlier the only way to use my API is to exclude the SecurityAutoConfiguration (#EnableAutoConfiguration(exclude = SecurityAutoConfiguration.class) but it is not a secure option.
Is there any way to resolve this?
From what I can see, it's likely that your SecurityConfig class never gets called, as it doesn't have any annotation indicating to Spring Boot that it should look for beans to autowire in the class (#Autowired)
To give you an idea, the following will never be called:
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
System.out.println("This will never called");
}
}
Whereas, if we had #EnableWebSecurity:
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
System.out.println("This is called");
}
}
Keep in mind that Spring Boot will not detect annotations inside a class if the class itself is not annotated with #Component or with another annotation that inherits the #Component annotation (such as #Configuration, #Service, ...)
EDIT: I quickly put together a program to imitate your situation:
SecurityConfiguration.java:
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser(new User("root", "root", Arrays.asList(new SimpleGrantedAuthority("USER"))))
.passwordEncoder(fakePasswordEncoder());
}
#Bean
public PasswordEncoder fakePasswordEncoder() {
return new PasswordEncoder() {
#Override
public String encode(CharSequence charSequence) {
return null; // matches(...) will always return true anyways
}
#Override
public boolean matches(CharSequence charSequence, String s) {
return true;
}
};
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/hello").permitAll()
.antMatchers("/test2").permitAll()
.antMatchers("/register").permitAll()
.antMatchers("/useraccount").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().permitAll()
.and()
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).permitAll();
}
}
Note that I just quickly made a password encoder that ignores the password because that would require more work
ExampleController.java:
#RestController
public class ExampleController {
#GetMapping("/")
public Object index() {
return getCurrentUser();
}
public Object getCurrentUser() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return ((UsernamePasswordAuthenticationToken)auth).getPrincipal();
}
}
And when I login with the username root and the any password (remember, the fake password encoder doesn't care about the password), it redirects me to / and displays the following:
{"password":null,"username":"root","authorities":[{"authority":"USER"}],"accountNonExpired":true,"accountNonLocked":true,"credentialsNonExpired":true,"enabled":true}
(which is normal because that's what I'm making it output)
I am trying to add Spring security to my project. I have custom login logic - advanced LDAP with custom encoding
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
//TODO
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
//TODO
}
private boolean login(String login, String pass) {
// custom login logic....
return loginHandler.login(login, pass);
}
}
Is there way, how to add login() method into configure method ?
It's just Spring Security configuration class. It shouldn't have a login() method in it, it should store the configuration only.
Probably you would like to have something like this: https://spring.io/guides/gs/authenticating-ldap/ - WebSecurityConfig should be good example.
Hello I'have a web application secured with Spring security, with a login page. This is my Security Configuration
#Configuration
#ComponentScan("it.besmart")
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
#Autowired
#Qualifier("customUserDetailsService")
UserDetailsService userDetailsService;
#Autowired
CustomSuccessHandler customSuccessHandler;
#Autowired
CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
#Autowired
DataSource dataSource;
#Autowired
private ConnectionFactoryLocator connectionFactoryLocator;
#Autowired
private UsersConnectionRepository usersConnectionRepository;
#Autowired
private FacebookConnectionSignup facebookConnectionSignup;
private final static Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
#Autowired
public void configureGlobalService(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Webapp security configured");
http.
authorizeRequests()
.antMatchers("/", "/register", "/registrationConfirm", "/resendRegistrationToken", "/park/**")
.permitAll()
.antMatchers("/edit/**", "/payment/**", "/plate/**", "/book/**", "/home", "/stop/**",
"/notification/**", "/include/**")
.access("hasRole('USER') or hasRole('ADMIN') or hasRole('PARK')").antMatchers("/admin/**")
.access("hasRole('ADMIN') or hasRole('PARK')").antMatchers("/updatePassword")
.hasAuthority("CHANGE_PASSWORD_PRIVILEGE")
.and().formLogin().loginPage("/")
.successHandler(customSuccessHandler).failureHandler(customAuthenticationFailureHandler)
.usernameParameter("email").passwordParameter("password").and().rememberMe()
.rememberMeParameter("remember-me").tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(86400).and().exceptionHandling().accessDeniedPage("/Access_Denied").and()
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/?logout=true").permitAll();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setDataSource(dataSource);
return db;
}
}
This works good by securing all my web application.
In the same application I have also a Resource/Authorization Server to protect some REST api.
Some resources are protected with an authorization code grant, so the untrusted Mobile App should take the access token from my application with a login form. I would like that the application use a different login page when trying to login from the Mobile App.
This is my resourceServer configuration
#EnableResourceServer
#ComponentScan("it.besmart.easyparking")
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig {
private final Logger logger = LoggerFactory.getLogger(ResourceServerConfig.class);
#Autowired
DataSource dataSource;
private static final String RESOURCE_ID = "easyparking_api";
#Configuration
// #Order(2)
public class grantCredentialsConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
logger.debug("Api security configured");
http
.requestMatchers().antMatchers("/api/oauth/**").and().authorizeRequests()
.antMatchers("/api/oauth/**").access("hasRole('USER')").and().formLogin().loginPage("/apilogin")
.permitAll();
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore()).resourceId(RESOURCE_ID);
}
}
#Configuration
// #Order(4)
public class clientCredentialsConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
logger.debug("Client security configured");
http
.requestMatchers().antMatchers("/oauth2/**", "/api/registration", "/api/park/**").and()
.authorizeRequests().antMatchers("/oauth2/**", "/api/registration", "/api/park/**").authenticated();
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore()).resourceId(RESOURCE_ID);
}
}
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
}
so, grantCredentialsConfiguration should redirect the requests to /apilogin form, but it does not, i am redirected to the main web app login page... How it can be accomplished?
EDIT
Looking closer into the logs, it looks like that when i try to hit /oauth/authorize/ the normal security chain takes place and i get
2017-05-25 12:23:15 DEBUG o.s.security.web.FilterChainProxy[310] - /oauth/authorize?response_type=token&client_id=test&redirect_uri=https://www.getpostman.com/oauth2/callback reached end of additional filter chain; proceeding with original chain
2017-05-25 12:23:15 DEBUG o.s.s.o.p.e.FrameworkEndpointHandlerMapping[310] - Looking up handler method for path /oauth/authorize
2017-05-25 12:23:15 DEBUG o.s.s.o.p.e.FrameworkEndpointHandlerMapping[317] - Returning handler method [public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map<java.lang.String, java.lang.Object>,java.util.Map<java.lang.String, java.lang.String>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)]
2017-05-25 12:23:15 DEBUG o.s.s.w.a.ExceptionTranslationFilter[163] - Authentication exception occurred; redirecting to authentication entry point
org.springframework.security.authentication.InsufficientAuthenticationException: User must be authenticated with Spring Security before authorization can be completed.
So it looks like searching for a handler to manage the request, instead of redirecting to /api/apilogin as desired, he finds an Authentication exception and so i go to the standard login page... But why i get this exception?
Its happening because you haven't specified the order of the security configuration classes.
In Spring security resources protection should be mentioned from specific to generic.
Class SecurityConfiguration is more generic than grantCredentialsConfiguration. As both protect following resources.
SecurityConfiguration protects /** (Default URL)
grantCredentialsConfiguration /api/oauth/**
Since the order is not defined, SecurityConfiguration's generic configuration hides the specific configuration by grantCredentialsConfiguration
To get these to work as expected you'll have to define the order as below.
#Configuration
#Order(2)//Generic config should have larger value (lower priority)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
}
#Configuration
#Order(1)//Specific with lower value (higher priority)
public class grantCredentialsConfiguration extends ResourceServerConfigurerAdapter {
}
Note: Since these login pages are not from different applications, they share the SecurityContextHolder or the security context. So if you login from one login page and then try to go the protected resource of the other, you won't be redirected to the next login page. Instead you'll get the 403 (depending on the roles assigned by the different login pages). At a time only one login session can be maintained.
Here's a sample on Github
https://github.com/ConsciousObserver/TestMultipleLoginPages.git
Have you tried adding URL path /apilogin , to the
.antMatchers("/", "/register", "/registrationConfirm",/resendRegistrationToken", "/park/**")
.permitAll()
I am guessing the application is redirecting the /apilogin access to the common authentication login page, since it is not added to the unauthenticated access list.
Right after registration (sign up) I'm logging in my user programmatically via Spring Security:
public register(HttpServletRequest request, String user, String password) {
...
request.login(user, password);
}
This works fine, but it doesn't create the remember-me cookie (although with interactive login the cookie is created fine).
Now I've read in this and this answer, that you have to wire in the implementation of RememberMeServices (I use PersistentTokenBasedRememberMeServices) and then call onLoginSuccess. I haven't been successful to autowire PersistentTokenBasedRememberMeServices.
How to make this work? Is this the right way? Why Spring Security doesn't offer a more convenient way?
P.S.: This is an excerpt from my configuration:
#Configuration
#EnableWebSecurity
public class WebSecConf extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.rememberMe()
.tokenRepository(new MyPersistentTokenRepository())
.rememberMeCookieName("rememberme")
.tokenValiditySeconds(60 * 60 * 24)
.alwaysRemember(true)
.useSecureCookie(true)
.and()
....
...
}
}
You didn't mention the Spring version. Below configuration will work with Spring 4 but you can modify it for other version. In your WebSecConf class autowire PersistentTokenRepository and UserDetailsService interfaces. Add Bean to get PersistentTokenBasedRememberMeServices instance.
#Configuration
#EnableWebSecurity
public class WebSecConf extends WebSecurityConfigurerAdapter {
#Autowired
PersistentTokenRepository persistenceTokenRepository;
#Autowired
UserDetailsService userDetailsService;
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.rememberMe()
.tokenRepository(persistenceTokenRepository)
.rememberMeCookieName("rememberme")
.tokenValiditySeconds(60 * 60 * 24)
.alwaysRemember(true)
.useSecureCookie(true)
.and()
....
...
}
#Bean
public PersistentTokenBasedRememberMeServices getPersistentTokenBasedRememberMeServices() {
PersistentTokenBasedRememberMeServices persistenceTokenBasedservice = new PersistentTokenBasedRememberMeServices("rememberme", userDetailsService, persistenceTokenRepository);
persistenceTokenBasedservice.setAlwaysRemember(true);
return persistenceTokenBasedservice;
}
}
Now in your Controller or class where you are doing programmatic login, autowire PersistentTokenBasedRememberMeServices and add below code inside the method to invoke loginSuccess method.
#Autowired
PersistentTokenBasedRememberMeServices persistentTokenBasedRememberMeServices;
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null){
persistentTokenBasedRememberMeServices.loginSuccess(request, response, auth);
}
I've stumbled on this issue and struggled a bit to get everything working correctly, for future reference this is how to set things up.
Define a RememberMeService bean configured to your needs.
Use TokenBasedRememberMeServices if you want a simple hash based token system or PersistentTokenBasedRememberMeServices if you'd rather persist the tokens to database. Both solutions are described in further details here : https://docs.spring.io/spring-security/site/docs/3.2.0.CI-SNAPSHOT/reference/html/remember-me.html
Please note that the constructor first argument is not the cookie name but the key used to validate remember-me tokens.
#Configuration
public class SecurityBeans {
#Autowire
PersistentTokenRepository persistenceTokenRepository;
#Autowired
UserDetailsService userDetailsService;
#Bean
public PersistentTokenBasedRememberMeServices getPersistentTokenBasedRememberMeServices() {
PersistentTokenBasedRememberMeServices persistenceTokenBasedservice = new TokenBasedRememberMeServices("remember-me-key", userDetailsService, persistenceTokenRepository);
persistenceTokenBasedservice.setCookieName("rememberme");
persistenceTokenBasedservice.setTokenValiditySeconds(60 * 60 * 24);
persistenceTokenBasedservice.setAlwaysRemember(true);
persistenceTokenBasedservice.setUseSecureCookie(true);
return persistenceTokenBasedservice;
}
}
You should inject the RememberMeService directly when configuring HttpSecurity. You also have to configure the exact same key as defined in your RememberMeService because the configurer also sets up the RememberMeAuthenticationProvider which checks that the remember-me token key generated by RememberMeService is correct.
#Configuration
#EnableWebSecurity
public class WebSecConf extends WebSecurityConfigurerAdapter {
#Autowired
RememberMeServices rememberMeServices;
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.rememberMe()
.rememberMeServices(rememberMeServices)
.key("remember-me-key")
.and()
....
...
}
}
And finally you should invoke RememberMeService's loginSuccess in your method doing the programmatic login as described in abaghel's answer.