I created a very simple http basic security for a Springboot app, the app deploys and I put the user and password. The problem is that if I call again with a different password, the request still counts as correct, instead of rejecting it.But if I change the user, then the application rejects my request and waits for the correct username and password.
my code:
#Configuration
public class SecurityConfig {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().anyRequest().authenticated().and().httpBasic();
return http.build();
}
}
The result with good credentials: The json output is as expected
The result with bad credentials: The json output is still as if it was succesfull
The result with different user: The app behaves like expected
This is expected for httpBasic(), see the BasicAuthenticationFilter.
After the first request, you should have a JSESSIONID cookie, which will be used to authenticate further requests. This behavior avoids doing the basic authentication flow every time a request comes in. The only reason to reauthenticate is if the HTTP Basic username is different from the authenticated user's username.
Related
I am currently using a rather simple approach to restrict a certain suburl (everything under /api/rest) and all of its subpaths via WebFluxSecurity. Some paths (everything directly under the root NOT in /api/rest) are excluded so that they can be access without authorization. However, sometimes the accessing party might send an empty authorization header which leads to unsecured endpoints returning a 401.
See the relevant code here:
#Configuration
#EnableWebFluxSecurity
public class SecurityConfiguration {
#Value(value = "${...}")
private String user;
#Value(value = "${...}")
private String pw;
#Bean
public MapReactiveUserDetailsService userDetailsService() {
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
UserDetails user = User
.withUsername(user)
.password(encoder.encode(pw))
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user);
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/api/rest/**")
.authenticated()
.anyExchange()
.permitAll()
)
.httpBasic(withDefaults());
return http.build();
}
}
On stackoverflow I've only found a few suggestions how to handle this with WebSecurity. However, this is not possible for me as I use webflux security.
See e.g.
Springboot webflux throwing 401 when Authorization header sent to unrestricted endpoint
Spring Boot 2: Basic Http Auth causes unprotected endpoints to respond with 401 "Unauthorized" if Authorization header is attached
TL;DR
If you pass invalid credentials to any endpoint with httpBasic() enabled, it will return a 401 response.
One important distinction that's relevant here is the difference between authentication and authorization. The httpBasic() DSL method adds the AuthenticationWebFilter configured for HTTP Basic. The authorizeExchange(...) DSL method defines authorization rules, such as authenticated() and permitAll().
The authentication filter appears earlier in the Spring Security filter chain than the authorization filter, and so authentication happens first which we would expect. Based on your comments, it seems you are expecting authentication not to happen if you mark an endpoint as permitAll(), but this is not the case.
Whether authentication is actually attempted against a particular request depends on how the authentication filter matches the request. In the case of AuthenticationWebFilter, a ServerWebExchangeMatcher (requiresAuthenticationMatcher) determines whether authentication is required. For httpBasic(), every request requires authentication. If you pass invalid credentials to any endpoint with httpBasic() enabled, it will return a 401 response.
Additionally, a ServerAuthenticationConverter (authenticationConverter) is used to read the Authorization header and parse the credentials. This is what would fail if an invalid token (or Authorization header) is given. ServerHttpBasicAuthenticationConverter is used for httpBasic() and is fairly forgiving of invalid header values. I don't find any scenarios that fail and produce a 401 response except invalid credentials.
TL;DR
Everytime localhost:4200 (through cors filter) makes a http request to localhost:8080 it loses sessionscope bean which holds the authentications which basically makes it failing all the calls with 403. Excluding the 1st http request (which isn't behind spring security)
I have a spring boot application that works well in localhost:8080.
We are creating an angular iframe inside of it, which also works well (when deployed on localhost:8080)
But when we do it on localhost:4200 (ng serve)
it wouldn't work
It started complaing about cors so i had the following configurations except everything about cors which i added.
#Configuration
#Profile({"dev"})
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class springDevConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception{
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
http.headers().cacheControl().disable();
http.cors().configurationSource(corsConfigurationSource())
.and().authorizeRequests().anyRequest().permitAll();
}
#Bean
public CorsConfigurationSource corsConfigurationSource(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(
ImmutableList.of("http://localhost:4200"));
configuration.setAllowedMethods(Arrays.asList(
RequestMethod.GET.name(),
RequestMethod.POST.name(),
RequestMethod.OPTIONS.name(),
RequestMethod.DELETE.name(),
RequestMethod.PUT.name()));
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
My session bean is fairly simple
#Component
#SessionScope
public class UserSession {
//Holds the Authorities for the prePostEnabled
User user;
...
}
To init the user i make a request to a certain endpoint (unprotected) and do something like this in the code
...
User user = new User(id, "", authorities);
Authentication auth = new UsernamePasswordAuthenticationToken(
user, null,authorities));
SecurityContextHolder.getContext().setAuthentication(auth);
Usersession.setUser(user);
...
When i make a http request on localhost:8080, the subsequent http requests has the same session.
But when i try from localhost:4200 making a request to localhost:8080 every requests seems to fetch a different UserSession / opens a new session perhaps? (giving me 403 on all the protected endpoints)
What is really happening and why is localhost:4200 when making a request to localhost:8080 making a new session with each call? (and what configs should be changed to fix such an issue?)
Addendum 1º: If i comment
#EnableGlobalMethodSecurity(prePostEnabled = true)
The code works well for localhost:4200 (i mean he stops having 403 codes) but probabily still is using another session scope bean in each request
Addendum 2º:
It works now
All i had to do was put ssl in the ng serve configuration (which it had at localhost:8080 but not 4200) and JSessionId started working!
Can you show a bit more of how do you embed the iframe and what serves the original page?
What seems to be happening is that you're effectively making a cross domain request without realizing it. 8080 and 4200 are different domains, and if parts of the page get loaded from one and parts (i.e. the iframe) from another, it constitutes a cross-domain request and the cookies sent by one domain do not get applied to the requests to the other, hence the session scope is not shared. Additionally, if you're making Ajax requests to a domain other than the one the original page was loaded from, they'd get rejected by default unless you set up CORS. This explains why you had to do that.
You must make sure all parts of the page (iframes and Ajax requests included) are loaded from the same domain, or that you have alternative means of attaching the session to the request. Normally, JSESSIONID cookie is used for this. Does this cookie get sent in the initial response?
You commonly have one app serving the front end including the initial page (e.g. the app on 4200), and one just responding to API calls (e.g. the app on 8080), with CORS configured. This way all the calls to the back end are done via the same domain and the cookies get applied normally.
Below is my SecurityConfiguration class, I am using.
#SpringBootApplication
#RestController
public class WebMvcConfig {
#Configuration
protected static class SecurityConfiguration extends
WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/**").authenticated()
.anyRequest().authenticated()
.and()
.formLogin().permitAll();
}
}
After startup, as soon as I hit my URL (http://localhost:8080/TestApp/), it takes me to the default login page and when I enter the default user Id (user) and password (printed on the console), it take me to my index.html page mapped by the "/" URL via my AngularJS routing. I am able to navigate through the UI but as soon as I submit any $http request (I am trying with a POST request), it gives me 403 on the browser console with my $http request URL.
Can someone help?
Error 403 means that you are forbidden from accessing all the resources.
If you inspect the error details, more probably you will have a message such as Expected CSRF token not found.
From v4 onwards, spring security enables by default csrf protection. This is a good practice as csrf attacks force an end user to execute unwanted actions on a web application in which they’re currently authenticated.
So in a dev environment, adding http.csrf().disable(); will solve your problem. But you should consider adding a csrf token when you want to move to a prod env.
I am using Spring Security to secure an application using OAuth. I wanted to make it stateless so I have a custom authentication filter that authenticates based on a token from the client. My configuration for Spring Security is as follows:
#Override
protected void configure(HttpSecurity http) throws Exception {
// require https
http.requiresChannel().antMatchers("/**").requiresSecure().and().portMapper().http(80).mapsTo(443);
http.antMatcher("/**").authorizeRequests().anyRequest().authenticated().and()
.cors().and()
.csrf().disable()
.addFilterBefore(customOAuth2Filter(), BasicAuthenticationFilter.class)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
As you can see, I am using SessionCreationPolicy.STATELESS which should mean that Spring Security never uses a session. However, if I login and then try to access a protected endpoint without a token(say, if I close the tab and then open a protected endpoint in a new tab, or if I just refresh the page), the application will give me the data at the endpoint instead of asking me to login again.
So it seems like a session is being used even though I am using SessionCreationPolicy.STATELESS. However, I did notice that the JSESSIONID of each request being sent does not match the JSESSIONID in the set-cookie header of the previous response. Not sure why that is or where the new JSESSIONID is coming from.
How do I make it so subsequent requests without the token will ask for login again? Any insight into what is happening here would be appreciated, thanks.
I have 2 apps running, one is resource server where I have the info that needs authentication to view the text. Then I have authorization server that gives tokens. Right now I can use postman or Insomnia, add the auth_url, token_url, client_id, client_secret and I get the token. I add the token to header and i get do a get request to my resource server using header, and it works just fine.
Now i have no idea how to implement redirection from my resource server directly. Like when I go to
localhost:9000/home
I'd like to get redirected to:
localhost:9001/login
where I login with my inmemory user then it redirects me back to localhost:9000/home and I see the message.
What would be the best way to implement a way for user to access information on localhost:9000/home. You go to localhost:9000/home, it goes to authorization server on localhost:9001, you log in with username and password. Approve the grant, and it puts you back to localhost:9000/home and then you can see the text, what was previously protected, because you didn't have token to access it.
ResourceServer.java
#SpringBootApplication
#RestController
#EnableResourceServer
#EnableOAuth2Client
public class SampleResourceApplication extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**").hasRole("user")
.anyRequest().authenticated();
}
#Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
public static void main(String[] args) {
SpringApplication.run(SampleResourceApplication.class, args);
}
#RequestMapping("/home")
public String home() {
return "this is home";
}
}
and my properties looks like:
server:
port: 900
security:
oauth2:
client:
client-id: foo
client-secret: foosecret
access-token-uri: http://localhost:9001/auth/oauth/token
user-authorization-uri: http://localhost:9001/auth/oauth/authorize
grant-type: USER
auto-approve-scopes: true
resource:
user-info-uri: http://localhost:9001/auth/user
Let's separate the agents: You have the user (i.e. you, also know as the resource owner), the authorization server, the resource server and the client (the application that access your urls, i.e. your browser).
Normally, this happens in your situation:
When your client access the resource server, it receives a 401. Depending of your implementation, you could also directly redirect the client to your AS (using a simple redirect response).
Your AS prompts you for credentials. After validating them, it issues a token for you. You can then use this token to access the RS.
What you're trying to get (if I understand correctly) is to redirect with the token automatically. To achieve this, you can simply pass the url you tried to reach (i.e. localhost:9000/home) when you redirect to your AS at the end of step 1. Your AS hten prompts the user for credentials, generate the token, stores it as a cookie (in the case of a browser), and redirects you to the url he received (localhost:9000/home).
EDIT: what's the resulting code for the redirection.
When you get to the configure, you first check if the user is authenticated. If he is, then all's fine, but if he isn't, you must catch this event and start your redirection. This can be done using the exceptionHandling method of the chaining http:
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**").hasRole("user")
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint());
}
private AuthenticationEntryPoint authenticationEntryPoint() {
return new AuthenticationEntryPoint() {
// You can use a lambda here
#Override
public void commence(HttpServletRequest aRequest, HttpServletResponse aResponse,
AuthenticationException aAuthException) throws IOException, ServletException {
aResponse.sendRedirect(MY_AS_URL + "?redirect_uri=localhost:9001/home");
}
};
}
Unfortunately, I am not familiar with the Spring framework, but hopefully this helps anyways:
OAuth is an authorization protocol. It does not handle authentication (see also: "What is the difference between authentication and authorization?" on ServerFault).
If I understand you correctly, you want users to be redirected to /login when they go to /home and aren't already logged-in. This step has nothing to do with OAuth, it must be part of your application's security / firewall setup.
Please also note that there is a difference between logging in (authenticating) on the authorization server and actually granting the application the right to access your resources (authorization). First you have to prove who you are and only then can you give access to your stuff. These are two separate steps.