Authentication is null on the SecurityContextHolder.getContext(); - java

I am trying to add Facebook authorization using Spring Security in Spring Boot app. Currently, my problem is extracting data from Principal.
Here is my security config:
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure (HttpSecurity http) throws Exception {
http
.csrf().disable()
.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.deleteCookies("JSESSIONID")
.clearAuthentication(true)
.logoutSuccessUrl("/").permitAll();
}
#Bean
public PrincipalExtractor facebookPrincipalExtractor(){
return new FacebookPrincipalExtractor();
}
}
and principal extractor:
public class FacebookPrincipalExtractor implements PrincipalExtractor {
#Autowired
UserService userService;
#Override
public Object extractPrincipal(Map<String, Object> map) {
String name = (String) map.get("name");
String id = (String) map.get("id");
User user = userService.findOne(id);
if (user == null) {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
String token = ((OAuth2AuthenticationDetails) authentication.getDetails()).getTokenValue();
user = new User();
FacebookClient facebookClient = new DefaultFacebookClient(token, Version.VERSION_2_10);
JSONObject object = facebookClient.fetchObject("me", JSONObject.class);
// userService.createUser(object);
}
return user;
}
}
After login, the Map<String, Object> map contains only the name and id. Call to securityContext.getAuthentication() returns null.
Moreover, if I create something similar to the endpoint and pass the Principal there as a parameter, then this will work. Example:
#RequestMapping("/user")
public Principal user(Principal principal) {
return principal;
}
The principal will contain all the necessary data.
In this regard, 2 questions:
Why security context does not contain authentication?
Where does the principal come from if it is passed as a parameter to a method?
This is what the debug looks like inside

Although SecurityContextHolder.getContext() is never null the authentication it contains is cleared once a request is completed. What this means is that if you try to access it during a process which goes through the spring web security it will be there. But as soon as the request finishes the following gets logged
SecurityContextHolder now cleared, as request processing completed
and the authentication is set to null. Any attempts to access it directly through the SecurityContext outside of an http request will result in a null.

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
authentication.getPrincipal();
use nested call for getting authentication object and then getPrincipal(); will return current loggedin user details

Related

NullPointer trying to get user info with Keycloak in Spring Boot

I am using Keycloak in my REST application with spring-boot.
It works correctly, I have defined roles in Keycloak, then in my Config.class I allow access to the end-points that interest me according to the role of each user. I have the problem when trying to retrieve the user information in my back (name, principal, authorities...).
I have read various SO POSTS like these:
how-to-get-principal-from-a-keycloak-secured-spring-boot-application
nullpointer-when-securing-spring-boot-rest-service-with-keycloak
but I can't find any that work for me. If I use SecurityContext or KeycloakAuthenticationToken, I get null when calling the getAuthentication method.
In my code, in Controller class I have the following:
private SecurityContext securityContext = SecurityContextHolder.getContext();
#GetMapping(value = "/getLicenses")
public ResponseEntity<List<License>> getLicenses() {
System.out.println("SecurityContext: " + securityContext);
System.out.println("Authentication securityContext: " + securityContext.getAuthentication());
return new ResponseEntity<List<License>>(licenseService.getLicenses(), null, HttpStatus.OK);
}
The output from console after access to /getLicenses is:
SecurityContext: org.springframework.security.core.context.SecurityContextImpl#ffffffff: Null authentication
Authentication securityContext: null
My KeycloakSecurityConfig.class
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity( prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/getLicenses").hasAnyRole("GUEST", "ADMIN", "SUPERADMIN")
.antMatchers("/getRevision/{id}").hasRole("SUPERADMIN")
.permitAll();
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
It works fine, if I try to access to /getRevision/{id} end-point with a 'GUEST' role user, it can't permit it.
My .yml has a BD connection, JPA and keycloak connection:
keycloak.auth-server-url : XXXX
keycloak.realm: XXXX
keycloak.resource: login
keycloak.public-client: true
And Keycloak has 3 users with 3 different roles (1 rol each user). I can do login with Postman
Does anyone know why I get null when trying to access SecurityContext?
My goal is to retrieve user data
You are initializing the securityContext object in your controller when the controller is first created:
private SecurityContext securityContext = SecurityContextHolder.getContext();
At that time, there is no security context available, meaning that the context is always null.
Don't use a private variable in a controller to store the context, instead, fetch it from the SecurityContextHolder in the getLicenses method:
#GetMapping(value = "/getLicenses")
public ResponseEntity<List<License>> getLicenses() {
SecurityContext securityContext = SecurityContextHolder.getContext();
System.out.println("SecurityContext: " + securityContext);
System.out.println("Authentication securityContext: " + securityContext.getAuthentication());
return new ResponseEntity<List<License>>(licenseService.getLicenses(), null, HttpStatus.OK);
}

Invalid password is accepted in spring security using custom Authentication Provider

I am using spring security with custom Authentication Provider using basic auth.
When I am trying to hit backend API GET call through postman it is working fine only when I make changes in username
Here is the problem statement - whenever I modify the user name then only custom authenticator provider works. once I added the correct username and password then it works but after that when I am making any changes in password (giving wrong password) always showing 200 success response. If I am making changes in username (giving wrong username) then only call to custom authenticator provider happened and getting 401 response.
Java Spring code
#Configuration
#EnableWebSecurity
#ComponentScan("com.authentication.service")
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter{
#Autowired
private AuthService authProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception { http
.httpBasic().and().logout().clearAuthentication(true).and() .authorizeRequests()
.antMatchers("/index.html", "/", "/home", "/login", "/assets/**").permitAll()
.anyRequest().authenticated() .and() .csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); }
}
#Service
public class AuthService implements AuthenticationProvider{
#Override
public Authentication authenticate(Authentication authentication)
throws org.springframework.security.core.AuthenticationException {
String userName = (String) authentication.getPrincipal();
String userPassword = authentication.getCredentials().toString();
if(authenticateUser(userName, userPassword)) {
return new UsernamePasswordAuthenticationToken(userName, userPassword, new ArrayList<>());
} else {
return null;
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
public Boolean authenticateUser(String userName, String userPassword) {
// Using some third party service
// return true if user is authenticated else false
}
}
This is occurring because Spring Security is creating a session that is returned as a Cookie upon successful authentication (You can check this in the Cookies panel in the Postman's response). For Further requests, even if you provide invalid credentials, Postman will send this session cookie that will be used for Authentication.
To remove this effect, you can update your session management policy to be SessionCreationPolicy.STATELESS, this will make sure no session is created by the application and the Basic Auth credentials that are sent in the request are used for authentication.
You can update the Session Management Policy like this:
#Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}

Microservices Spring Cloud Gateway + Spring Security LDAP as SSO + JWT - Token lost between request/response

I am developing a microservice ecosystem using spring-boot. The microservices which are in place at the moment :
Spring Cloud Gateway - Zuul (responsible also for authorization requests downstream for microservices - extracting tokens from requests and validates whether the user has the right role to perform requests),
SSO using spring security LDAP ( responsible for authenticate user and generate JWT tokens) , SSO has also just a login page using thymeleaf
Web interface using Thymeleaf without login page ( not sure if I should use here spring security, at the moment)
Another microservice which provides data to web ui based on request from the browser
Discovery services using Eureka
The idea is filtering all the requests on the gateway for validating and forward the requests. If the user is not authenticated or token is experied then forward the user to SSO for login.
The firewall will expose only the port on Gateway side then others one will be theirs ports blocked using firewall rules.
Now i am blocked without knowing where to go or if I should move the SSO together with the gateway ( conceptually wrong but it might be a workaround if i do not find any solution)
Following the issue : The user hits the gateway (ex. http://localhost:7070/web) then the gateway forward the user to (ex. http://localhost:8080/sso/login), after the credentials have been validated , the SSO creates the JWT tokens and add it to the Header of response.
Afterwards the SSO redirect the request back to the gateway (ex. http://localhost:7070/web).
Until here, everything works fine but when the request reaches the gateway there is no 'Authorization' header on request which means NO JWT token.
So the gateway should extract the token, check the credentials and forward the request to the Web interface (ex. http://localhost:9090)
I am aware that using Handler on SSO to redirect request won't work at all due to 'Redirect' from spring will remove the token from the header before redirect.
But I do not know whether there is another way to set again the JWT on the header after Spring has removed it from the request or not.
Is there any conceptually issue on the architecture side? How can I forward the JWT to the gateway for being checked?
SSO
#EnableWebSecurity
public class SecurityCredentialsConfig extends WebSecurityConfigurerAdapter {
#Value("${ldap.url}")
private String ldapUrl;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
// Stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin()
.loginPage("/login")
// Add a handler to add token in the response header and forward the response
.successHandler(jwtAuthenticationSuccessHandler())
.failureUrl("/login?error")
.permitAll()
.and()
// handle an authorized attempts
.exceptionHandling()
.accessDeniedPage("/login?error")
.and()
.authorizeRequests()
.antMatchers( "/dist/**", "/plugins/**").permitAll()
.anyRequest().authenticated();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.groupSearchBase("ou=groups")
.userSearchFilter("uid={0}")
.groupSearchBase("ou=groups")
.groupSearchFilter("uniqueMember={0}")
.contextSource()
.url(ldapUrl);
}
#Bean
public AuthenticationSuccessHandler jwtAuthenticationSuccessHandler() {
return new JwtAuthenticationSuccessHandler();
}
}
public class JwtAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
#Autowired
private JwtConfig jwtConfig;
#Autowired
private JwtTokenService jwtTokenService;
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException, ServletException {
String token = jwtTokenService.expiring(ImmutableMap.of(
"email", auth.getName(),
"authorities", auth.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.map(Object::toString)
.collect(Collectors.joining(","))));
response.addHeader(jwtConfig.getHeader(), jwtConfig.getPrefix() + token);
DefaultSavedRequest defaultSavedRequest = (DefaultSavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST");
if(defaultSavedRequest != null){
getRedirectStrategy().sendRedirect(request, response, defaultSavedRequest.getRedirectUrl());
}else{
getRedirectStrategy().sendRedirect(request, response, "http://localhost:7070/web");
}
}
}
Gateway
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtConfig jwtConfig;
#Value("${accessDeniedPage.url}")
private String accessDeniedUrl;
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.csrf().disable() // Disable CSRF (cross site request forgery)
// we use stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin()
.loginPage("/sso/login")
.permitAll()
.and()
// handle an authorized attempts
// If a user try to access a resource without having enough permissions
.exceptionHandling()
.accessDeniedPage(accessDeniedUrl)
//.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
// Add a filter to validate the tokens with every request
.addFilterBefore(new JwtTokenAuthenticationFilter(jwtConfig), UsernamePasswordAuthenticationFilter.class)
// authorization requests config
.authorizeRequests()
.antMatchers("/web/**").hasAuthority("ADMIN")
// Any other request must be authenticated
.anyRequest().authenticated();
}
}
#RequiredArgsConstructor
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
private final JwtConfig jwtConfig;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 1. get the authentication header. Tokens are supposed to be passed in the authentication header
String header = request.getHeader(jwtConfig.getHeader());
// 2. validate the header and check the prefix
if(header == null || !header.startsWith(jwtConfig.getPrefix())) {
chain.doFilter(request, response); // If not valid, go to the next filter.
return;
}
// If there is no token provided and hence the user won't be authenticated.
// It's Ok. Maybe the user accessing a public path or asking for a token.
// All secured paths that needs a token are already defined and secured in config class.
// And If user tried to access without access token, then he/she won't be authenticated and an exception will be thrown.
// 3. Get the token
String token = header.replace(jwtConfig.getPrefix(), "");
try { // exceptions might be thrown in creating the claims if for example the token is expired
// 4. Validate the token
Claims claims = Jwts.parser()
.setSigningKey(jwtConfig.getSecret().getBytes())
.parseClaimsJws(token)
.getBody();
String email = claims.get("email").toString();
if(email != null) {
String[] authorities = ((String) claims.get("authorities")).split(",");
final List<String> listAuthorities = Arrays.stream(authorities).collect(Collectors.toList());
// 5. Create auth object
// UsernamePasswordAuthenticationToken: A built-in object, used by spring to represent the current authenticated / being authenticated user.
// It needs a list of authorities, which has type of GrantedAuthority interface, where SimpleGrantedAuthority is an implementation of that interface
final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
email, null, listAuthorities
.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()));
// 6. Authenticate the user
// Now, user is authenticated
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (Exception e) {
// In case of failure. Make sure it's clear; so guarantee user won't be authenticated
SecurityContextHolder.clearContext();
}
// go to the next filter in the filter chain
chain.doFilter(request, response);
}
}
#Component
public class AuthenticatedFilter extends ZuulFilter {
#Override
public String filterType() {
return PRE_TYPE;
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() throws ZuulException {
final Object object = SecurityContextHolder.getContext().getAuthentication();
if (object == null || !(object instanceof UsernamePasswordAuthenticationToken)) {
return null;
}
final UsernamePasswordAuthenticationToken user = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
final RequestContext requestContext = RequestContext.getCurrentContext();
/*
final AuthenticationDto authenticationDto = new AuthenticationDto();
authenticationDto.setEmail(user.getPrincipal().toString());
authenticationDto.setAuthenticated(true);
authenticationDto.setRoles(user.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList())); */
try {
//requestContext.addZuulRequestHeader(HttpHeaders.AUTHORIZATION, (new ObjectMapper()).writeValueAsString(authenticationDto));
requestContext.addZuulRequestHeader(HttpHeaders.AUTHORIZATION, (new ObjectMapper()).writeValueAsString("authenticationDto"));
} catch (JsonProcessingException e) {
throw new ZuulException("Error on JSON processing", 500, "Parsing JSON");
}
return null;
}
}
There is an issue about JWT. It is called "Logout Problem". First you need to understand what it is.
Then, check TokenRelay filter (TokenRelayGatewayFilterFactory) which is responsible for passing authorization header to downstream.
If you look at that filter, you will see that JWTs are stored in ConcurrentHashMap (InMemoryReactiveOAuth2AuthorizedClientService). The key is session, the value is JWT. So, session-id is returned instead of JWT header as the response provided.
Until here, everything works fine but when the request reaches the
gateway there is no 'Authorization' header on request which means NO
JWT token.
Yes. When the request comes to gateway, TokenRelay filter takes session-id from request and find JWT from ConcurrentHashMap, then it passes to Authorization header during downstream.
Probably, this flow is designed by spring security team to address JWT logout problem.

Spring security baisc authentication only validating first request

I'm using spring basic authentication with a custom authentication provider:
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider authProvider;
#Override
protected void configure(
AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.httpBasic();
}
And
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
if (customauth()) { // use the credentials
// and authenticate against the third-party system
{
return new UsernamePasswordAuthenticationToken(
name, password, new ArrayList<>());
}
} else {
return null;
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(
UsernamePasswordAuthenticationToken.class
);
}
To test this I'm using postman with the following tests:
invalid credentials -> 401 unauthorized
correct credentials -> 200 OK
invalid credentials -> 200 OK
My problem is that the last request should return 401 unauthorized and every following request after a successful login is 200 OK even with a wrong token and without token.
Thanks in advance.
When you logged in successfully, Spring Security will create an Authentication object and will put it in SecurityContext in your HTTP session. As far as you have a valid session with a valid Authentication object at the server, Spring Security won't authenticate your request again and will use the Authentication object saved in your session.
This is a Spring Security feature, see SEC-53:
Check the SecurityContextHolder for an authenticated Authentication and reuse it in that case, do not call the authentication manager again.
If you like to reauthenticate, you could
use no session at all
logout before reauthenticate
In both cases Spring Security will not find an authenticated user saved in the session and will use the new username and password for authentication.

Why SpringSecurity, after a logout, keeps giving the same authenticated Principal

So I am using Spring Security with Spring Boot. I wanted to make my own AuthenticationProvider, that was using the db in my very own way, so I did it with this authenticate method:
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String email = authentication.getName();
String password = authentication.getCredentials().toString();
UserWithEmail userWithEmail = authService.getUserByEmail(email);
if (userWithEmail == null)
return null;
if (userWithEmail.getPassword().equals(password)) {
UsernamePasswordAuthenticationToken authenticated_user = new UsernamePasswordAuthenticationToken(userWithEmail, password, Arrays.asList(REGISTERED_USER_SIMPLE_GRANTED_AUTHORITY));
return authenticated_user;
} else {
return null;
}
}
This, if I use the default /login page with the form, works well and after that if I add the following ModelAttribute to a Controller, it gets correctly filled with the UserWithEmail object:
#ModelAttribute("userwithemail")
public UserWithEmail userWithEmail(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
if (principal instanceof UserWithEmail)
return (UserWithEmail) principal;
else
return null;
}
The problem is that if I hit /login?logout, it correctly displays that I am logged out, but if I go through the controller again I still get the same UserWithEmail object as principal and it has the property authenticated=true
This is my Java config for spring security:
http
.formLogin()
.defaultSuccessUrl( "/" )
.usernameParameter( "username" )
.passwordParameter( "password" )
.and()
.logout().invalidateHttpSession(true).deleteCookies("JSESSIONID").permitAll().and()
.authorizeRequests()
.antMatchers("*/**").permitAll()
.antMatchers("/static/**").permitAll()
.antMatchers("/profile").hasRole(MyAuthenticationProvider.REGISTERED_USER_AUTH)
.and().authenticationProvider(getAuthProvider());
I'm new to Spring Security so maybe I'm missing something... can anyone help?
According to the docs here for CSRF POST is mandatory for logging out, together with the CSRF token for attack protection.
Because I am using a custom templating engine I had to intercept the CSRF token in a model attribute from the request, like this:
#ModelAttribute("crsf_token")
public CsrfToken getcrsfToken(HttpServletRequest request, Model model) {
CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
return token;
}
because it was not getting copied in the model for my templating engine.

Categories

Resources