I have configured a JDBC data source and autowired the JDBCTemplate to execute custom SQL queries. I also have a simple HTTP Basic authentication:
auth.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
However, I would like to use the user and password used for HTTP Basic to authenticate the user to the data base itself, i.e pass through the credentials of HTTP Basic to the data source and execute queries as the user who logged in with HTTP Basic authentication. I'm facing two issues here, one is that the username and password are in the application.properties file that I want to override every time a user authenticates and also (reload?) execute queries as that user instead of the ones specified in the properties file.
Update 1:
I could programmatically use username and password like below:
#Bean
#Primary
public DataSource dataSource() {
return DataSourceBuilder
.create()
.username("")
.password("")
.url("")
.driverClassName("")
.build();
}
But how to call this every time a user logs with the HTTP Basic auth with those credentials?
Use UserCredentialsDataSourceAdapter as #"M. Deinum" have suggested with some kind of filter or handling AuthenticationSuccessEvent.
Basically you should just call setCredentialsForCurrentThread method with current principal username and password.
You'll have to disable credential erasure for authentication manager in order to be able to retrieve user password after authentication.
#EnableWebSecurity
public static class Security extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.eraseCredentials(false) // for password retrieving
.inMemoryAuthentication()
.withUser("postgres").password("postgres1").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests().mvcMatchers("/").fullyAuthenticated();
}
}
Datasource adapter:
#Bean
public UserCredentialsDataSourceAdapter dataSource(DataSourceProperties properties) {
final UserCredentialsDataSourceAdapter dataSourceAdapter = new UserCredentialsDataSourceAdapter();
dataSourceAdapter.setTargetDataSource(DataSourceBuilder.create()
.driverClassName(properties.getDriverClassName())
.url(properties.getUrl())
.username(properties.getUsername())
.password(properties.getPassword())
.type(SimpleDriverDataSource.class) // disable pooling
.build());
((SimpleDriverDataSource) dataSourceAdapter.getTargetDataSource()).setDriverClass(org.postgresql.Driver.class); //binder won't set it automatically
return dataSourceAdapter;
}
AuthenticationSuccessHandler:
#Component
public static class AuthenticationHandler /*implements ApplicationListener<AuthenticationSuccessEvent> use that if your spring version is less than 4.2*/ {
private final UserCredentialsDataSourceAdapter dataSourceAdapter;
#Autowired
public AuthenticationHandler(UserCredentialsDataSourceAdapter dataSourceAdapter) {
this.dataSourceAdapter = dataSourceAdapter;
}
#EventListener(classes = AuthenticationSuccessEvent.class)
public void authenticationSuccess(AuthenticationSuccessEvent event) {
final Authentication authentication = event.getAuthentication();
final User user = (User) authentication.getPrincipal();
dataSourceAdapter.setCredentialsForCurrentThread(user.getUsername(), user.getPassword()); // <- the most important part
}
}
Or you can use Filter instead of event listener:
#Component
public static class DataSourceCredentialsFilter extends GenericFilterBean {
private final UserCredentialsDataSourceAdapter dataSourceAdapter;
#Autowired
public DataSourceCredentialsFilter(UserCredentialsDataSourceAdapter dataSourceAdapter) {
this.dataSourceAdapter = dataSourceAdapter;
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
final User user = (User) authentication.getPrincipal();
dataSourceAdapter.setCredentialsForCurrentThread(user.getUsername(), user.getPassword());
chain.doFilter(request, response);
dataSourceAdapter.removeCredentialsFromCurrentThread();
}
}
See full example here.
Related
I want to rewrite my Vaadin application to Vaadin 21.
With the Vaadin starter builder (https://vaadin.com/start) I created a simple app.
Currently my main struggle is to apply my simple CustomAuthenticationProvider to the Security manager to able to use the #RolesAllowed({ "user", "admin","USER"}) annotation.
Main problem that my AuthToken is generated somewhere else...
Its generate somewhere an empty Granted Authrities and ignore my custom AuthProvider code.
Question:
How to nicely handle role based access control?
Where I can use this annotation correctly:
#RolesAllowed({ "user", "admin","USER"})
public class ProfileView extends VerticalLayout {
Console after login:
UsernamePasswordAuthenticationToken [Principal=c.farkas, Credentials=[PROTECTED], Authenticated=false, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=DDE103F559B2F64B917753636B800564], Granted Authorities=[]]
xxx[USERcica, admin, USER]
??UsernamePasswordAuthenticationToken [Principal=c.farkas, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[USERcica, admin, USER]]
SecurityConfiguration.java
#EnableWebSecurity
#Configuration
public class SecurityConfiguration extends VaadinWebSecurityConfigurerAdapter {
#Autowired
private RequestUtil requestUtil;
#Autowired
private VaadinDefaultRequestCache vaadinDefaultRequestCache;
#Autowired
private ViewAccessChecker viewAccessChecker;
#Autowired
CustomAuthenticationProvider customAuthenticationProvider;
public static final String LOGOUT_URL = "/";
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
http.csrf().ignoringRequestMatchers(requestUtil::isFrameworkInternalRequest);
// nor with endpoints
http.csrf().ignoringRequestMatchers(requestUtil::isEndpointRequest);
// Ensure automated requests to e.g. closing push channels, service
// workers,
// endpoints are not counted as valid targets to redirect user to on
// login
http.requestCache().requestCache(vaadinDefaultRequestCache);
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry urlRegistry = http
.authorizeRequests();
// Vaadin internal requests must always be allowed to allow public Flow
// pages
// and/or login page implemented using Flow.
urlRegistry.requestMatchers(requestUtil::isFrameworkInternalRequest).permitAll();
// Public endpoints are OK to access
urlRegistry.requestMatchers(requestUtil::isAnonymousEndpoint).permitAll();
// Public routes are OK to access
urlRegistry.requestMatchers(requestUtil::isAnonymousRoute).permitAll();
urlRegistry.requestMatchers(getDefaultHttpSecurityPermitMatcher()).permitAll();
// all other requests require authentication
urlRegistry.anyRequest().authenticated();
// Enable view access control
viewAccessChecker.enable();
setLoginView(http, LoginView.class, LOGOUT_URL);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// Custom authentication provider - Order 1
auth.authenticationProvider(customAuthenticationProvider);
// Built-in authentication provider - Order 2
/* auth.inMemoryAuthentication().withUser("admin").password("{noop}admin#password")
// {noop} makes sure that the password encoder doesn't do anything
.roles("ADMIN") // Role of the user
.and().withUser("user").password("{noop}user#password").credentialsExpired(true).accountExpired(true)
.accountLocked(true).roles("USER");*/
}
#Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
web.ignoring().antMatchers("/images/*.png");
}
}
CustomAuthenticationProvider.java
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
System.out.println(authentication);
try {
// LdapContext ldapContext =
ActiveDirectory.getConnection(username, password);
List<GrantedAuthority> authorityList = new ArrayList<GrantedAuthority>();
authorityList.add(new SimpleGrantedAuthority("USER" + "cica"));
authorityList.add(new SimpleGrantedAuthority("admin"));
authorityList.add(new SimpleGrantedAuthority("USER"));
System.out.println("xxx"+authorityList.toString());
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
username, password, authorityList);
System.out.println("??" + usernamePasswordAuthenticationToken);
String id = VaadinSession.getCurrent() != null ? VaadinSession.getCurrent().getSession().getId() : "";
return usernamePasswordAuthenticationToken;
} catch (NamingException e) {
// e.printStackTrace();
// throw new CortexException("Authentication failed");
throw new BadCredentialsException("Authentication failed");
}
}
#Override
public boolean supports(Class<?> aClass) {
return aClass.equals(UsernamePasswordAuthenticationToken.class);
}
}
You must add the ROLE_ prefix to tell Spring Security that the GrantedAuthority is of type role.
authorityList.add(new SimpleGrantedAuthority("ROLE_USER" + "cica"));
authorityList.add(new SimpleGrantedAuthority("ROLE_admin"));
authorityList.add(new SimpleGrantedAuthority("ROLE_USER"));
We have a legacy Spring application (A) (that is not using spring-boot) that handles authentication and writes the session to Redis using spring-session (the data in Redis is stored as XML).
We now want to introduce a new application (B), using spring-boot 2.2.6.RELEASE and spring-session Corn-RC1, that should be useable if a user has signed into (A) with ROLE_ADMIN. I.e. this can be regarded as a very crude way of doing single sign on. A user should never be able to authenticate in B (it'd like to disable authentication if possible), it should only check that an existing user is authenticated in the session repository (redis) and has ROLE_ADMIN. Both A and B will be located under the same domain so cookies will be propagated by the browser. I've tried various different ways of getting this to work, for example:
#Configuration
#EnableWebSecurity
class ServiceBSpringSecurityConfig : WebSecurityConfigurerAdapter() {
#Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
}
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.formLogin()
.and()
.httpBasic().disable()
}
}
but this will show the default login screen:
I've also tried removing this part entirely:
#Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
}
but then it'll generate a default user and password and it does not seem to call the configure method (or the configuration doesn't work regardless).
How can I solve this?
What you need is to disable formLogin and httBasic on Application B and add a filter before spring's authentication filter AnonymousAuthenticationFilter or UsernamePasswordAuthenticationFilter. In the custom filter you will extract the cookie/header/token from the request object and based on that reach out to the redis cache for session details. This filter would then validate the session and create object of type org.springframework.security.core.Authentication and set that in the current SpringSecurityContext.
Below is the sudo code for this;
ServiceBSpringSecurityConfig
#Configuration
#EnableWebSecurity
public class ServiceBSpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(authEntryPoint()).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.httpBasic().disabled().and()
.formLogin().disabled().and()
.authorizeRequests().anyRequest().hasRole("ADMIN")
http.addFilterBefore(authTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
#Bean
public AuthTokenFilter authTokenFilter() {
return new AuthTokenFilter();
}
#Bean
public AuthEntryPoint authEntryPoint() {
return new AuthEntryPoint()
}
}
AuthEntryPoint
public class AuthEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPoint.class);
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// Very generic authEntryPoint which simply returns unauthorized
// Could implement additional functionality of forwarding the Application A login-page
logger.error("Unauthorized error: {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
}
}
AuthTokenFilter
public class AuthTokenFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// extract some sort of token or cookie value from request
token = request.getHeader("Token");
if (token != null) {
// Validate the token by retrieving session from redis cache
// Create org.springframework.security.core.Authentication from the token
Authentication auth = authFactory.getAuthentication(token);
// Set the spring security context with the auth
SecurityContextHolder.getContext().setAuthentication(auth);
} else {
// Do something if token not present at all
}
// Continue to to filter chain
filterChain.doFilter(request, response);
}
}
As mentioned this is sudo code so some adjustment might be required. However the general gist of token based auth remains the same.
Hi I have a Rest WS using WebSecurityConfigurerAdapter to implement HTTP Basic auth.
The password is allowed to be updated and I need to let the WS to pick up updated password without restarting server
Following are the codes:
SecurityConfig
// init a user with credentials admin/password
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
//disable csrf
.csrf().disable()
//authentic all requests
.authorizeRequests().anyRequest().authenticated().and().httpBasic()
//disable session
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(inMemoryUserDetailsManager());
}
#Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
Properties users = new Properties();
users.put("admin", "password,USER,enabled");
return new InMemoryUserDetailsManager(users);
}
}
The controller that will update password
#RestController
public class someController{
#Autowired
public InMemoryUserDetailsManager inMemoryUserDetailsManager;
// update password from password -> pass
#RequestMapping(...)
public updatePass(){
ArrayList<GrantedAuthority> grantedAuthoritiesList = new ArrayList<>();
grantedAuthoritiesList.add(new SimpleGrantedAuthority("USER"));
this.inMemoryUserDetailsManager.updateUser(new User("admin", "pass", grantedAuthoritiesList));
}
// another way that also doesn’t work
#RequestMapping(...)
public newUpdate(){
ArrayList<GrantedAuthority> grantedAuthoritiesList = new ArrayList<>();
grantedAuthoritiesList.add(new SimpleGrantedAuthority("USER"));
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("admin", "pass",
grantedAuthoritiesList);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(auth);
SecurityContextHolder.setContext(context);
}
}
After calling updatePass() with credential admin/password for the first time, I can see that the password has been updated to "pass" in debugger
I assume that if I'm to call updatePass() again, I should use admin/pass. However it turned out to be still using the old admin/password.
Sources I referred to when writing this code source1 source2
*I'm using Advance Rest Client to make the calls
When you update the password, you have to set the UserDetails in springSecurityContext object if the user is authenticated.
instead of using SecurityContext, I overwrote function loadUserByUsername of interface UserDetailsService to let spring security always pick up the latest pwd from DB.
I have a react app running on a separate port (localhost:3000) that i want to use to authenticate users with, currently i have a proxy setup to my Spring backend (localhost:8080).
Can I somehow manually authenticate instead of http.httpBasic() by sending a POST request to my backend and getting back a session cookie then include the cookie with every request? It would simplify the auth process on iOS side aswell (using this process i could only store the session cookie value in keychain and pass it with every request made to my api)
How would I disable csrf for non-browser requests?
Is there a better approach to this? Diffrent paths for browser and mobile auth?
{
"username": "user",
"password": "12345678"
}
Handle the request in spring controller
#PostMapping(path = "/web")
public String authenticateUser() {
//Receive the auth data here... and perform auth
//Send back session cookie
return "Success?";
}
My WebSecurityConfigurerAdapter.java
#Configuration
#EnableWebSecurity
public class WebsecurityConfig extends WebSecurityConfigurerAdapter {
private final DetailService detailService;
public WebsecurityConfig(DetailService detailService) {
this.detailService = detailService;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(detailService).passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST,"/api/v1/authenticate/new").permitAll()
.antMatchers(HttpMethod.POST,"/api/v1/authenticate/web").permitAll()
.anyRequest().authenticated();
}
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "POST").allowedOrigins("http://localhost:8080");
}
};
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(14);
}
}
You can create an endpoint that takes user's credentials in a request body, perform authentication and then set tokens, and other required parameters in HttpOnly cookies.
After setting cookies, subsequent requests can read access/refresh token from cookies and add it in requests, you can then use custom CheckTokenEndpoint to verify tokens.
In the following example TokenParametersDto is a POJO that has username and password properties.
For issuing token (by verifying credentials) you can delegate call to TokenEndpoint#postAccessToken(....) or use its logic to your own method.
#PostMapping(path = "/oauth/http/token", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> issueToken(#RequestBody final #Valid #NotNull TokenParametersDto tokenParametersDto,
final HttpServletResponse response) {
final OAuth2AccessToken token = tokenService.issueToken(tokenParametersDto);
storeTokenInCookie(token, response);
return new ResponseEntity<>(HttpStatus.OK);
}
private void storeTokenInCookie(final OAuth2AccessToken token, final HttpServletResponse response) {
final Cookie accessToken = new Cookie("access_token", token.getValue());
accessToken.setHttpOnly(true);
accessToken.setSecure(sslEnabled);
accessToken.setPath("/");
accessToken.setMaxAge(cookieExpiration);
final Cookie tokenType = new Cookie("token_type", token.getTokenType());
tokenType.setHttpOnly(true);
tokenType.setSecure(sslEnabled);
tokenType.setPath("/");
tokenType.setMaxAge(cookieExpiration);
// Set Refresh Token and other required cookies.
response.addCookie(accessToken);
response.addCookie(tokenType);
}
Check this answer for disabling CSRF for a specific URL section.
I have been going square trying to implement a new RESTful web service using Spring.IO. I've worked through perhaps 30 different online examples that suppose to provide this example but none of them worked 'out of the box'.
A further complication is that 95% of examples use XML configuration exclusively, which in my personal opinion is not as readable as a pure java configuration.
After a great many hours I have managed to cobble together something that 'works' but I would very much be interested in feedback as to my particular implementation. Specifically:
Assuming the client authorisation token is not compromised is my implementation secure.
I was unable to correctly AutoWire the AuthenticationTokenProcessingFilter class in WebSecurityConfig due to the constructor requiring an AuthenticationManager. If there is a way to do this that would help clean things up a little.
The main application class:
#ComponentScan({"webservice"})
#Configuration
#EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
The WebSecurityConfig class (AFAIK this does the majority of the job previously performed by the XML):
#Configuration
#EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
AuthenticationTokenProcessingFilter authenticationTokenProcessingFilter;
#Autowired
CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
authenticationTokenProcessingFilter =
new AuthenticationTokenProcessingFilter(authenticationManager());
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests().antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(authenticationTokenProcessingFilter, AnonymousAuthenticationFilter.class)
.httpBasic().authenticationEntryPoint(customAuthenticationEntryPoint);
}
}
The AuthenticationTokenProcessingFilter which performs the token authentication on client connect:
public class AuthenticationTokenProcessingFilter extends GenericFilterBean {
AuthenticationManager authManager;
public AuthenticationTokenProcessingFilter(AuthenticationManager authManager) {
this.authManager = authManager;
}
#Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
#SuppressWarnings("unchecked")
Map<String, String[]> parms = request.getParameterMap();
if(parms.containsKey("authToken")) {
String token = parms.get("authToken")[0];
// Validate the token
User user = TokenUtils.getUserFromToken(token);
// If we managed to get a user we can finish the authentication
if (user!=null) {
//Add a default authority for all users
List<GrantedAuthority> grantedAuths = new ArrayList();
grantedAuths.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
// build an Authentication object with the user's info
AbstractAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, token, grantedAuths);
// set the authentication into the SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
// continue thru the filter chain
chain.doFilter(request, response);
}
}
The TokenUtils class used by AuthenticationTokenProcessingFilter:
public class TokenUtils {
/**
* Get an authorisation token for the provided userId
*
* #param userId
* #return
*/
public static String getToken(long userId) {
throw new UnsupportedOperationException("Not implemented yet!");
}
/**
* Attempt to get a user for the provided token
*
* #param token
* #return User if found, otherwise null
*/
public static User getUserFromToken(String token) {
throw new UnsupportedOperationException("Not implemented yet!");
}
}
The CustomAuthenticationEntryPoint, which returns a 403 if the user was not successfully authenticated:
#Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Authentication token was either missing or invalid." );
}
}
An finally my web service entry points:
#RestController
public class EntryPoints {
#RequestMapping(value = "/login", method={RequestMethod.POST})
public LoginResponse login(#RequestParam(value="username", required=true) String username,
#RequestParam(value="password", required=true) String password) {
LoginRequest loginRequest = new LoginRequest(username, password);
//Authenticate the user using the provided credentials
//If succesfull return authentication token
//return new LoginResponse(token);
throw new UnsupportedOperationException("Not implemented yet!");
}
#RequestMapping(value = "/account", method={RequestMethod.POST})
public AccountResponse account(#RequestParam(value="accountId", required=true) long accountId) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//Return the request account information
throw new UnsupportedOperationException("Not implemented yet!");
}
}