I am using spring security for authentication
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider).authenticationProvider(secondaryAuthProvider) ;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/login").hasAnyRole("ADMIN","VISITOR").and().
formLogin().defaultSuccessUrl("/login").failureUrl("/")
.loginPage("/login").usernameParameter("username").passwordParameter("password").failureUrl("/").
and().logout().permitAll().and().exceptionHandling().accessDeniedPage("/403").and()
.authorizeRequests().antMatchers("/resources/**").permitAll().and().authorizeRequests().
antMatchers("/api/**").authenticated().and().httpBasic().realmName("MY_TEST_REALM").
authenticationEntryPoint(getBasicAuthEntryPoint());
}
#Bean
public CustomBasicAuthenticationEntryPoint getBasicAuthEntryPoint(){
return new CustomBasicAuthenticationEntryPoint();
}
This is working fine. When i hit /api/login i am able to use basic authentication
But after first successful authentication I am able to use /api/login without authentication.
It is not taking me to auth provider at second time. First time control is going there but not second time.
Register two WebSecurity configurations:
#Configuration
#EnableWebSecurity
#Order(1)
public class StatefulConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider).authenticationProvider(secondaryAuthProvider) ;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).and()
.antMatcher("/web/*").authorizeRequests()
.antMatchers("/*").hasAnyRole("ADMIN","VISITOR").and().
formLogin().defaultSuccessUrl("/web/login").failureUrl("/web/error").loginPage("/web/login").usernameParameter("username").passwordParameter("password").failureUrl("/").
and().logout().logoutUrl("/web/logout").permitAll().and().exceptionHandling().accessDeniedPage("/403").and()
.authorizeRequests().antMatchers("/resources/**").permitAll();
}
}
And for rest:
#Configuration
#Order(2)
public class StatelessConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider).authenticationProvider(secondaryAuthProvider) ;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.antMatcher("/api/*").authorizeRequests()
.antMatchers("/api/**").authenticated().and().httpBasic().realmName("MY_TEST_REALM").
authenticationEntryPoint(getBasicAuthEntryPoint());
}
#Bean
public CustomBasicAuthenticationEntryPoint getBasicAuthEntryPoint(){
return new CustomBasicAuthenticationEntryPoint();
}
}
Be careful: there are antMatcher(...) and antMatchers(...) methods.
UPDATE: similar problem & solution here
Session is created when you log in. Session will be active until you logout (destroy session), or when time expire.
See example
EDIT:
Spring application have a few important settings associated with session.
The first one is session creation policy (by default IF_REQUIRED - if session linked with request already exists it will be not destroyed and created again).
Session is saved in cookie - you can check it hitting f12.
Application "check" does cookie exist in request. When you go to login page there are two cases:
you don't have session -> login popup appears, you can log in,
you have session because SecurityContextHolder contain information about current session.
How does it work?
When you use .httpBasic(), Spring Security registers BasicAuthenticationFilter. In method doFilterInternal you can see:
if (authenticationIsRequired(username)) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, tokens[1]);
authRequest.setDetails(
this.authenticationDetailsSource.buildDetails(request));
Authentication authResult = this.authenticationManager
.authenticate(authRequest);
if (debug) {
this.logger.debug("Authentication success: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
After success first login, authentication is set.
When you try to log in again authenticationIsRequired method returns false. Why?
Look at the source:
private boolean authenticationIsRequired(String username) {
// Only reauthenticate if username doesn't match SecurityContextHolder and user
// isn't authenticated
// (see SEC-53)
Authentication existingAuth = SecurityContextHolder.getContext()
.getAuthentication();
if (existingAuth == null || !existingAuth.isAuthenticated()) {
return true;
}
// Limit username comparison to providers which use usernames (ie
// UsernamePasswordAuthenticationToken)
// (see SEC-348)
if (existingAuth instanceof UsernamePasswordAuthenticationToken
&& !existingAuth.getName().equals(username)) {
return true;
}
// Handle unusual condition where an AnonymousAuthenticationToken is already
// present
// This shouldn't happen very often, as BasicProcessingFitler is meant to be
// earlier in the filter
// chain than AnonymousAuthenticationFilter. Nevertheless, presence of both an
// AnonymousAuthenticationToken
// together with a BASIC authentication request header should indicate
// reauthentication using the
// BASIC protocol is desirable. This behaviour is also consistent with that
// provided by form and digest,
// both of which force re-authentication if the respective header is detected (and
// in doing so replace
// any existing AnonymousAuthenticationToken). See SEC-610.
if (existingAuth instanceof AnonymousAuthenticationToken) {
return true;
}
return false;
}
As you can see getAuthhentication invoked on SecurityContextHolder return object set in previous request.
Sorry for my bad English.
UPDATE: you can invalidate session using "/logout" url.
Related
We are implementing a role-based security API (bearer-token only) with spring-boot and Keycloak.
The security config looks as follows:
#KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
/**
* Registers the KeycloakAuthenticationProvider with the authentication manager.
*/
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(keycloakAuthenticationProvider());
}
#Bean
public KeycloakConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
/**
* Defines the session authentication strategy.
* For bearer-only applications there is no session needed and therefor
* we use the NullAuthenticatedSessionStrategy.
*/
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Override
protected void configure(HttpSecurity http) throws Exception
{
super.configure(http);
http.authorizeRequests()
.anyRequest()
.permitAll();
}
}
I retrieve roles with
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof KeycloakAuthenticationToken) {
SimpleKeycloakAccount user = (SimpleKeycloakAccount) auth.getDetails();
for (String role : user.getRoles()) {
// Clean and collect roles...
}
}
This works and we get the roles. The problem is, that we even get roles when no request header "Authorization: Bearer [token]" has been send. This happens, when we have two consecutive calls, the first with valid token, then the second call (without Authorization header) has the same KeycloakAuthenticationToken with the same roles.
My questions are now:
How can it be that the Authorization header is null, but the security context still returns a KeycloakAuthenticationToken?
Shouldn't the security context be per thread, and each thread exists during a single http request only?
Update
I found a workaround in the meantime, that is ugly but solves the problem for the moment. I've written a custom HandlerInterceptor, that cleans the security context, if no Authorization header can be found:
if (request.getHeader("Authorization") == null)
SecurityContextHolder.clearContext();
Although this seems to solve the issue, it shouldn't be necessary. So something is still strange, I guess.
I'm struggling to implement the jwt authentication while i understand (i think so) the theory behind it.
The whole idea is to send a jwt token every time user wants to access personal data through backend secured endpoints, instead of sending the credentials every time.
My backend is based on this repo. I have edited the code for my needs:
JwtTokenFilter.java
public class JwtTokenFilter extends OncePerRequestFilter {
private JwtTokenProvider jwtTokenProvider;
public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
// Save initial path. Since CustomErrorController always uses /error path
// we can't retrieve the initial path using httpRequest.getRequestURI() method
httpServletRequest.setAttribute("initialPath", httpServletRequest.getRequestURI());
if (httpServletRequest.getHeader("Authorization") == null) {
throw new TokenException("Authorization field is empty.");
}
String token = jwtTokenProvider.resolveToken(httpServletRequest);
if (token == null) {
throw new TokenException("Non Bearer Token");
}
// throws TokenException
jwtTokenProvider.validateToken(token);
// throws NoUserFoundException
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
WebSecurityConfig.java
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtTokenProvider jwtTokenProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
// Disable CSRF (cross site request forgery)
http.csrf().disable();
// No session will be created or used by spring security
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Entry points
// http.authorizeRequests()
// .antMatchers("/signin").permitAll()
// .antMatchers("/signup").permitAll()
// Disallow everything else..
// .antMatchers("/profile").authenticated();
// If a user tries to access a resource without having enough permissions
// http.exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> {
// System.out.println("**************** INSIDE HANDLEEEEEEEEER");
// response.sendError(417);
// });
// IMPORTANT!!!
// DO I NEED THESE???
// http.authorizeRequests().antMatchers("/signin", "signup").permitAll();
// http.authorizeRequests().antMatchers("/signin", "signup").hasAnyRole("ADMIN", "USER");
// http.authorizeRequests().antMatchers("/signin", "signup").hasAnyRole("ADMIN");
// http.authorizeRequests().antMatchers("/profile").authenticated();
http.antMatcher("/profile").apply(new JwtTokenFilterConfigurer(jwtTokenProvider));
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Here are my questions:
See the commented out code on the WebSecurityConfigurerAdapter. First of, it doesn't seem to work. But even if it did, it wouldn't make sense.
We want the jwt filter to be applied only to specific urls. It wouldn't make sense to ask for a token when everyone has the right to sign up or sign in. The last line inside WebSecurityConfigurerAdapter applies the filter only on the /profile endpoint.
The jwt token contains (if it is valid) the username. When extracted, we search for that user on the database. This user object contains its' role. So, in order to have a user's role we MUST pass through the jwt filter in order to validate it and THEN find the saved user.
So why the code doesn't work at all?
Is the commented code really useless or am i missing something? How would it be possible to authenticate the user before even applying the jwt filter?
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.
I'm trying to make a basic authentication service, for some business logic i need to acceept all basic auth credentials and make them hit another service (and there it will be fail if the credentials are wrong).
So I'm trying to throw an exception when the basic auth is not present, or are empty credentials.
This is my SecurityConfigurer:
#Configuration
#EnableWebSecurity
public class SecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
#Autowired
STGAuthenticationProvider authProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and().httpBasic();
}
}
And this is my CustomAuthProvider:
#Component
public class STGAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
if(!StringUtils.isBlank(username) && !StringUtils.isBlank(password)) {
return new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>());
} else {
throw new STGNoCredentialsException(Constants.Error.NO_CREDENTIALS);
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
Actually my app gives me "401 Unauthorized" if i send a request with no auth (I would really like to get my custom Exception you can see at my CustomAuthProvider).
And when i send just 1 credential (username or password), or no one, my service answer me with empty body at POSTMAN. Can you guys help me?
From what I understand, your issue is similar to one I had a few days ago: I needed to return a 401 instead of a 403 whenever an endpoint was called with no authorisation or with auth token expired.
With respect to your code, I would add .exceptionHandling().authenticationEntryPoint(...) to your WebSecurityConfigurerAdapter as follows
#Configuration
#EnableWebSecurity
public class SecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
/* other stuff */
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and().httpBasic()
.exceptionHandling().authenticationEntryPoint(/*custom exception*/);
}
}
and then, instead of /*custom exception*/ add something as new MyAuthException(), where MyAuthException looks like the following:
#Component
public class MyAuthException implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) /*throws ...*/ {
response.setStatus(/* your status */);
response.getWriter().write(/*the body of your answer*/);
/* whatever else you want to add to your response */
/* or you could throw an exception, I guess*/
}
}
(I don't remember and right now I can't check whether this class needs to be marked as #Component, I think not).
I have a front end written in AngularJS, and a Spring MVC backend. The idea I had was to only secure the REST API services and use an interceptor in AngularJS to redirect the user to the login page when an unauthorized service call is made. The problem I'm facing now is that, while a service is called, the page is briefly displayed before the user is redirected. Is there anything I can do about that? Or is this approach fundamentally flawed?
This is the interceptor:
$httpProvider.interceptors.push(function ($q, $location) {
return {
'responseError': function(rejection) {
var status = rejection.status;
if (status == 401 || status == 403) {
$location.path( "/login" );
} else {
}
return $q.reject(rejection);
}
};});
My security configuration:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/**")
.authorizeRequests()
.anyRequest().authenticated();
}
#Bean(name="myAuthenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
The login controller:
#RequestMapping(value = "/login", method = RequestMethod.POST, produces="application/json")
#ResponseBody
public String login(#RequestBody User user) {
JSONObject result = new JSONObject();
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
try {
Authentication auth = authenticationManager.authenticate(token);
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(auth);
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession(true);
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
result.put("isauthenticated", true);
} catch (BadCredentialsException e) {
result.put("isauthenticated", false);
}
return result.toString();
}
I think this approach is OK, but you may have to live with the page flash, which in turn means you will have to handle it gracefully.
I guess the page flush happens roughly as follows:
A navigation takes place, rendering a template and activating a controller for the new route
The controller calls a service; this is asynchronous, so the page without any data is displayed
The service returns 401/403, it is intercepted and the new navigation to the login page occurs
You may want to try:
Collecting all data required by the page in the resolve configuration of the route (supported both by ngRoute and angular-ui-router), so that the navigation will not complete before all data is fetched.
Handle it from within the page: while the service call is still pending, display a spinner/message whatever to let the user know that some background activity is going on.
When the interceptor catches a 401/403, have it open a modal popup, explaining the situation and offering the user to login or navigate to the login page as a single option. Combine this with the spinner/message.