CSRF Prevention with Spring Security and AngularJS - java

I'm using Spring 4.3.12.RELEASE Version, AngularJS 1.4.8. I'm trying to prevent the CSRF Attack on the application.
#Configuration
#Order(2)
public static class SecurityConfig extends WebSecurityConfigurerAdapter {
String[] pathsToRemoveAuthorizaton = {
"/mobile/**",
"/logout",
"/j_spring_security_logout",
"/login",
};
private final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
#Override
public void configure(HttpSecurity http) throws Exception {
logger.info("http configure");
http.antMatcher("/**").authorizeRequests().antMatchers(pathsToRemoveAuthorizaton).permitAll()
.antMatchers("/**").authenticated()
.and().formLogin().loginPage("/login")
.usernameParameter("employeeId").passwordParameter("password")
.successForwardUrl("/dashboard").defaultSuccessUrl("/dashboard", true)
.successHandler(customAuthenticationSuccessHandler()).loginProcessingUrl("/j_spring_security_check")
.and().logout().logoutSuccessUrl("/logout").logoutUrl("/j_spring_security_logout")
.logoutSuccessHandler(customLogoutSuccessHandler()).permitAll().invalidateHttpSession(true)
.deleteCookies("JSESSIONID").and().sessionManagement().sessionFixation().newSession()
.maximumSessions(1).maxSessionsPreventsLogin(true).and()
.sessionCreationPolicy(SessionCreationPolicy.NEVER).invalidSessionUrl("/logout").and()
.exceptionHandling().accessDeniedPage("/logout");
// http.csrf().csrfTokenRepository(csrfTokenRepository()).and()
// .addFilterAfter(new StatelessCSRFFilter(), CsrfFilter.class);
http.csrf().csrfTokenRepository(csrfTokenRepository());
// http.csrf().disable();
// http.csrf().ignoringAntMatchers("/mobile/**");
http.authorizeRequests().anyRequest().authenticated();
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
#Bean
public AuthenticationSuccessHandler customAuthenticationSuccessHandler() {
return new CustomAuthenticationSuccessHandler();
}
#Bean
public LogoutSuccessHandler customLogoutSuccessHandler() {
return new CustomLogoutSuccessHandler();
}
}
Below is my angular service code
govtPMS.service('Interceptor', function($q, $location, $rootScope, pinesNotifications, Auth) {
return {
request: function(config) {
config.headers.Authorization = 'Bearer '+$rootScope.authToken;
// document.cookie = 'CSRF-TOKEN=' + $rootScope.generateKey;
return config;
},
requestError: function (rejection) {
return $q.reject(rejection);
},
response: function(res) {
if(res.status === 200 || res.status === 201){
if(res.data.response !== undefined){
if(res.data.status === 1 || res.data.status === 3 || res.data.status === 2) {
pinesNotifications.notify({
'title': 'Success',
'text': res.data.message,
'type': 'success',
'delay': 5000
});
}
else if(res.data.status === 5) {
pinesNotifications.notify({
'title': 'Warning',
'text': res.data.message,
'type': 'warning',
'delay': 5000
});
}
}
}
return res || $q.when(res);
},
responseError: function(error) {
return $q.reject(error);
}
};
}).config(['$httpProvider', function($httpProvider) {
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-TOKEN';
$httpProvider.defaults.xsrfCookieName = 'CSRF-TOKEN';
$httpProvider.interceptors.push('Interceptor');
}])
I'm still not able to see the CSRF Token header along with requests.
In this application, we are using 3 jsp pages - login.jsp, logout.jsp and dashboard.jsp
angular scope is defined in the dashboard.jsp, hence login and logout are out of scope of AngularJS.
I've also tried it the stateless way from this and this examples, where angular is generating a UUID and appending with cookie and request header, the below filter was doing the job fine.
Until the logout attack. In this attack, the attacker is trying to succesfully logout the user since to logout from the application, we are simply using a href.
<li></i><span>Logout</span></li>
Now since its logout is out of angular, the angularjs interceptor is cannot attach the UUID there.
I've been struggling on this since past week, Any help will be appreciated.
StatelessCSRFFilter.java
package com.leadwinner.sms.config.filters;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.web.filter.OncePerRequestFilter;
public class StatelessCSRFFilter extends OncePerRequestFilter {
private static final String CSRF_TOKEN = "CSRF-TOKEN";
private static final String X_CSRF_TOKEN = "X-CSRF-TOKEN";
private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
List<String> excludedUrls = new ArrayList<>();
excludedUrls.add("/resources");
excludedUrls.add("/j_spring_security_check");
excludedUrls.add("/j_spring_security_logout");
excludedUrls.add("/login");
excludedUrls.add("/logout");
excludedUrls.add("/mobile");
excludedUrls.add("/migrate");
excludedUrls.add("/dashboard");
String path = request.getServletPath();
System.out.println(path);
AtomicBoolean ignoreUrl = new AtomicBoolean(false);
excludedUrls.forEach(url -> {
if (request.getServletPath().startsWith(url.toLowerCase()) || request.getServletPath().equals("/")) {
ignoreUrl.set(true);
}
});
if (!ignoreUrl.get()) {
final String csrfTokenValue = request.getHeader(X_CSRF_TOKEN);
final Cookie[] cookies = request.getCookies();
System.out.println("**************************************************");
System.out.println("--------------------------------------------------");
String csrfCookieValue = null;
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(CSRF_TOKEN)) {
csrfCookieValue = cookie.getValue();
}
}
}
System.out.println("csrfTokenValue = "+csrfTokenValue);
System.out.println("csrfCookieValue = "+csrfCookieValue);
System.out.println("--------------------------------------------------");
System.out.println("**************************************************");
if (csrfTokenValue == null || !csrfTokenValue.equals(csrfCookieValue)) {
accessDeniedHandler.handle(request, response, new AccessDeniedException(
"Missing or non-matching CSRF-token"));
return;
}
}
filterChain.doFilter(request, response);
}
}

If the request can be made by the browser and credentials be handed up automatically (session cookie, basic auth creds), then CSRF protection is necessary, even with a mobile API.
Given that you have a mobile API as part of the application, the question is can those APIs be successfully addressed by the browser?
What I'd recommend is that you create two separate filter chains, like so, one for the web app and one for the mobile API:
#Configuration
#Order(100)
public class WebAppConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) {
http
.requestMatchers()
.antMatchers("/app/**")
.and()
.authorizeRequests()
// ...
.anyRequest().authenticated()
.and()
.formLogin();
}
}
#Configuration
#Order(101)
public class MobileApiConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) {
http
.requestMatchers()
.antMatchers("/api/**")
.and()
.authorizeRequests()
// ...
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt();
}
}
What this achieves is it separates the two configurations. The endpoints relative to the web application use one setup, and the endpoints relative to the mobile API use another setup.
Now, a couple of comments about the mobile API. I'm assuming that you are authenticating using OAuth 2.0 Bearer tokens, which is why the configuration above uses oauth2ResourceServer() from Spring Security 5.1+. What this does is selectively disables CSRF for requests that contain an Authorization: Bearer xyz header.
But, since you are using Spring Security 4.3, then you may need to do something more like the following (unless you can upgrade):
#Order(101)
public class MobileApiConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) {
http
.requestMatchers()
.antMatchers("/api/**")
.and()
.authorizeRequests()
// ...
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(NEVER)
.and()
.addFilterBefore(new MyMobileAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf().disable();
}
}
What you'd need to make sure of, though, is that your custom authentication filter doesn't use an authentication mechanism that is automatically sent by the browser from any origin (session cookies, Authorization: Basic).

`Hi Shiva,
Your code in the configure method of SecurityConfig should look like the below code:
http
.authorizeRequests()
.antMatchers(patterns)
.permitAll()
.antMatchers("/hello/**")
.hasRole("USER")
.and()
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.requireCsrfProtectionMatcher(csrfProtectionMatcher(patterns))
.and()
.httpBasic()
.and()
.addFilterAfter(csrfFilter(patterns), FilterSecurityInterceptor.class)
.addFilterAfter(new StatelessCSRFFilter(), CsrfFilter.class);
And in the StatelessCSRFFilter, use the following code:
#Override
public void init(FilterConfig filterConfig) throws ServletException {}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
CsrfToken csrf = (CsrfToken) servletRequest.getAttribute(CsrfToken.class.getName());
String token = csrf.getToken();
if (token != null && isAuthenticating(servletRequest)) {
HttpServletResponse response = (HttpServletResponse) servletResponse;
Cookie cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
filterChain.doFilter(servletRequest, servletResponse);
}
private boolean isAuthenticating(ServletRequest servletRequest) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
return request.getRequestURI().equals("/login");
}`

<pre>
Add this code for the csrfTokenRepository method
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
Add this for the csrfFilter method
private Filter csrfFilter(String[] patterns) {
CsrfFilter csrfFilter = new CsrfFilter(csrfTokenRepository());
csrfFilter.setRequireCsrfProtectionMatcher(csrfProtectionMatcher(patterns));
return csrfFilter;
}
Add this for the csrfProtectionMatcher method
private NoAntPathRequestMatcher csrfProtectionMatcher(String[] patterns) {
return new NoAntPathRequestMatcher(patterns);
}
Also remove these lines in configure method
.csrfTokenRepository(csrfTokenRepository())
.requireCsrfProtectionMatcher(csrfProtectionMatcher(patterns))
Move these lines below .csrf() in configure method:
.and()
.addFilterAfter(csrfFilter(patterns), FilterSecurityInterceptor.class)
</pre>

Related

How to bypass spring security on an authenticated endpoint for specific domain?

I am using jwt token based spring security.
I have an endpoint '/sample-endpoint' which requires authentication. However, I need to bypass security for this endpoint when the request comes from a specific domain called xyz.com.
Is it possible to do so? If so, how to do that?
Here is what I have so far.
SecurityConfig
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// cant add the end point here as it would open up for everybody.
public static final String[] unauthedUrls = { "healthcheck","some-other-endpoint"}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.disable()
.csrf()
.disable()
.cors()
.and()
.headers().frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.addFilterAfter(jwtSecurityFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers(unauthedUrls)
.permitAll()
.anyRequest()
.authenticated();
}
Here is JwtSecurityFilter implementation.
public class JwtSecurityFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtSecurityFilter.class);
private static final String JWT_PREFIX = "Bearer ";
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
setAuthenticationContext(request);
chain.doFilter(request, response);
}
private void setAuthenticationContext(HttpServletRequest request) {
try {
String token = getJwt(request);
if (StringUtils.isBlank(token)) {
throw new RuntimeException("Authorization token not provided");
}
// some logic here...
} catch (Exception ex) {
if (request != null && Arrays.stream(SecurityConfig.unauthedUrls).anyMatch(url -> request.getRequestURI().contains(url))) {
// it's a URL that isn't authenticated so an exception here is normal
// if we couldn't get a token
return;
}
LOGGER.warn("Unable to authenticate request: {} {}", ex.getMessage(), request == null ? null : request.getRequestURI());
}
}
private String getJwt(HttpServletRequest request) {
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StringUtils.isBlank(authHeader) || !authHeader.startsWith(JWT_PREFIX)) {
return "";
}
return authHeader.replaceFirst(Pattern.quote(JWT_PREFIX), "");
}
}
What you want is to ignore certain URLs for this override the configure method that takes WebSecurity object and ignores the pattern. For example, using the api:
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/v1/signup");
}
And remove that line from the HttpSecurity part. This will tell Spring Security to ignore this URL and don't apply any filters to them.
I have a better way:
http
.authorizeRequests()
.antMatchers("/api/v1/signup/**").permitAll()
.anyRequest().authenticated()

How do i whitelist api in JWT Filter

I have SpringSecurityWebAppConfig class that uses my own filter class JwtAuthenticationFilter.
The question now is how do i bypass my JwtAuthenticationFilter for api calls that does not have request header and/or token. Do I set it as a configurable and read it in the filter?
My JwtAuthenticationFilter is an imported class from another library. The purpose is to reuse the file for the other microservices.
Example:
/scanFile does not need request token. When I add into SpringSecurityWebAppConfig. My filter is throw 401 as it does not have request token.
SpringSecurityWebAppConfig class:
public class SpringSecurityWebAppConfig extends WebSecurityConfigurerAdapter {
#Autowired
public JwtAuthenticationFilter jwtAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/homePage").access("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
.antMatchers("/userPage").access("hasRole('ROLE_USER')")
.antMatchers("/adminPage").access("hasRole('ROLE_ADMIN')")
.antMatchers(HttpMethod.DELETE, "/data").access("hasRole('ROLE_ADMIN')")
.antMatchers("/login").permitAll()
.antMatchers("/logout").authenticated()
.and()
.anonymous().disable()
.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.and()
.headers()
.httpStrictTransportSecurity()
.includeSubDomains(true).maxAgeInSeconds(31536000);
// Add a filter to validate the tokens with every request
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
JwtAuthenticationFilter Class:
private void getJwtFromRequest(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String bearerToken = request.getHeader("Authorization");
// Step 1: Check if bearer token exist in authorization header and if bearer token start with "Bearer "
if (!StringUtils.hasText(bearerToken) || !bearerToken.startsWith("Bearer ")) {
String errorMsg = "No access token found in request headers.";
Error err = new Error(JWT_AUTHENTICATION_FILTER, "AccessTokenMissingException", errorMsg);
// Java object to JSON string
String jsonString = mapper.writeValueAsString(err);
log.error(jsonString);
throw new AccessTokenMissingException(errorMsg);
}
//rest of the processing here ...
}
so you want to whitelist some apis even without spring security authentication?
you can use web.ignoring().antMatchers() in your SpringSecurityWebAppConfig, to exclude the urls from spring security.
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/scanFile/**”);
}

Multiple security configurations for specific endpoints

I am wondering if there is a way to provide two separate types of authentication?
User should log, register, get user data for endpoints /login, /register, /user using basic auth. And when I call /api it should only be authenticated with JWT token provided in headers.
But when I call /api I get all data without any authentication. When user is logged and call /user, API gives JWT to access /api.
My code:
Configuration for basic auth:
#Configuration
#EnableWebSecurity
#Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable();
http
.authorizeRequests()
.antMatchers("/user").authenticated()
.antMatchers("/register").permitAll()
.and()
.formLogin().permitAll()
.defaultSuccessUrl("/user");
}
Configuration for JWT auth:
#Configuration
#Order(2)
public class JWTSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.antMatcher("/api/**")
.addFilterAfter(new JWTAuthorizationFilter(),UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic().disable();
}
I had the same problem, I wanted basic Authentication for some endpoints and for some other I wanted other authentication methods. like yours. you wanna basic authentication for some of the endpoints (/login,/register, /user ) and JWT authentication for some other(/api/**).
I used some tutorials about multiple entry points in spring security but it didn't work.
So here is my solution (It worked)
Separate basic authentication from JWT authentication by creating a custom filter.
Add a prefix path for the endpoints that should be authenticated using basic authentication. like :
(/basic/login, /basic/register,/basic/user)
Create a new custom filter for /basic prefix (for /basic requests) and check basic authentication
#Component
public class BasicAuthenticationFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//Check for the requests that starts with /basic
if (httpServletRequest.getRequestURI().startsWith("/basic/")) {
try {
//Fetch Credential from authorization header
String authorization = httpServletRequest.getHeader("Authorization");
String base64Credentials = authorization.substring("Basic".length()).trim();
byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
String credentials = new String(credDecoded, StandardCharsets.UTF_8);
final String username = credentials.split(":", 2)[0];
final String password = credentials.split(":", 2)[1];
//Check the username and password
if (username.equals("admin") && password.equals("admin")) {
//continue
chain.doFilter(request, response);
} else
throw new AuthenticationCredentialsNotFoundException("");
} catch (Exception e) {
throw new AuthenticationCredentialsNotFoundException("");
}
} else chain.doFilter(request, response);
}
}
Write main security configuration just for JWT and permit /basic URL
#Configuration
#EnableWebSecurity
public class JWTSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/basic/**").permitAll().and()
.csrf().disable()
.antMatcher("/api/**")
.addFilterAfter(new JWTAuthorizationFilter(),UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic().disable();
}

MissingCsrfTokenException: Could not verify the provided CSRF token because your session was not found

I am reading the spring documentation: Adding CSRF to Stomp Header
And I try to add stom header to the connect event but I get the error on client:
>>> CONNECT
XSRF-TOKEN:f86232c1-e877-46e9-b4e6-7427c3d89940
accept-version:1.1,1.0
heart-beat:10000,10000
<<< ERROR
message:Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]; nested exception is org.springframework.security.web.csrf.MissingCsrfTokenException\c Could not verify the provided CSRF token because your session was not found.
content-length:0
client code:
<script type="text/javascript" src="/webjars/js-cookie/js.cookie.js"></script>
var headers = {};
var headerName = "XSRF-TOKEN";
var token = Cookies.get('XSRF-TOKEN')
headers[headerName] = token;
stompClient.connect(headers, function (frame) {....});
websocket security config:
#Configuration
public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) {
// You can customize your authorization mapping here.
messages.anyMessage().authenticated();
messages.simpDestMatchers("/app/hello").authenticated()//.hasRole("ADMIN")
.simpSubscribeDestMatchers("/user/queue/**").hasRole("ADMIN")
.simpSubscribeDestMatchers("/topic/greetings").authenticated();
}
// TODO: For test purpose (and simplicity) i disabled CSRF, but you should re-enable this and provide a CRSF endpoint.
#Override
protected boolean sameOriginDisabled() {
return false; //! I do it especially
}
}
spring security config:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String SECURE_ADMIN_PASSWORD = "rockandroll";
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.loginPage("/index.html")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/sender.html")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/index.html")
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/js/**", "/lib/**", "/images/**", "/css/**", "/index.html", "/","/*.css","/webjars/**", "/*.js").permitAll()
.antMatchers("/websocket").hasRole("ADMIN")
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ADMIN")
.anyRequest().authenticated();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new AuthenticationProvider() {
#Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
List<GrantedAuthority> authorities = SECURE_ADMIN_PASSWORD.equals(token.getCredentials()) ?
AuthorityUtils.createAuthorityList("ROLE_ADMIN") : null;
return new UsernamePasswordAuthenticationToken(token.getName(), token.getCredentials(), authorities);
}
});
}
}
Maybe my csrf token header name wrong?
P.S.
This problem was not in Spring Framework. After updating to Spring-Boot (2.5.0, war+Tomcat 9 implementation, OAuth2) I have got the same problem.
It works for me:
Step 1 (add Security configuration for Websockets - you need for disabling SCRF Websocket Protection)
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
#Configuration
public class SocketSecurityConfig
extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected boolean sameOriginDisabled() {
return true;
}
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpTypeMatchers(SimpMessageType.CONNECT, SimpMessageType.HEARTBEAT, SimpMessageType.UNSUBSCRIBE, SimpMessageType.DISCONNECT).permitAll()
.anyMessage().permitAll()
.simpDestMatchers("/**").permitAll()
;
}
}
Step 2 (add Access to Websockets and Stomp in Web Security configuration):
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
#Slf4j
#EnableWebSecurity
#ComponentScan("com.example.your.project.package.name")
#Configuration
#RequiredArgsConstructor(onConstructor = #__(#Autowired))
public class OAuth2LoginSecurityConfig
extends WebSecurityConfigurerAdapter {
private static final String[] OPEN_URIS = {
"/**/*.woff",
"/**/*.woff2",
"/**/*.ttf",
"/webjars/**",
"/login",
"/login/*",
"/login/*/**",
"/resources/**",
"/error",
"/error/*",
"/error/*/**",
"/oauth2/authorization/**",
"/**/*.png",
"/**/*.css",
"/**/*.js",
"/style.css",
"/favicon.ico",
"/wicket",
"/wicket/resource/",
"/wicket/resource/*",
"/wicket/resource/*/**",
"/wicket/resources/**",
"/wicket/resources/*/**",
"/wicket/*",
"/wicket/page/*",
"/wicket/page/*/**",
"/webjars",
"/webjars/*",
"/webjars/*/**",
"/webservices/websocket/",
"/webservices/websocket/*",
"/webservices/websocket/*/**",
"/webservices/websocketHandlerEndpoint",
"/webservices/websocketHandlerEndpoint/*",
"/webservices/websocketHandlerEndpoint/*/**"
};
/////---Here some Configuration (not showed)------------ /////
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.headers()
.frameOptions().sameOrigin()
.httpStrictTransportSecurity().disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.and()
.authorizeRequests(authorize -> authorize.antMatchers(OPEN_URIS).permitAll())
// (Optional : )
.oauth2Login(oauth2 ->
oauth2.permitAll() // <-- actually not real, but here must be code
//....`enter code here`..
//
)
//.....
.logout()
// ...
;
}
//another part of Configuration .....
}
Note:
1)"/webservices/websocket/..." -is Stomp endpoint
2) "/webservices/websocketHandlerEndpoint..." -is Websocket handler endpoint

How to logout oauth2 client in Spring?

I have the simplest oauth2 client:
#EnableAutoConfiguration
#Configuration
#EnableOAuth2Sso
#RestController
public class ClientApplication {
#RequestMapping("/")
public String home(Principal user, HttpServletRequest request, HttpServletResponse response) throws ServletException {
return "Hello " + user.getName();
}
public static void main(String[] args) {
new SpringApplicationBuilder(ClientApplication.class)
.properties("spring.config.name=application").run(args);
}
}
I also have the following application.yml:
server:
port: 9999
servlet:
context-path: /client
security:
oauth2:
client:
client-id: acme
client-secret: acmesecret
access-token-uri: http://localhost:8080/oauth/token
user-authorization-uri: http://localhost:8080/oauth/authorize
resource:
user-info-uri: http://localhost:8080/me
logging:
level:
org.springframework.security: DEBUG
org.springframework.web: DEBUG
It is the full code. I don't have any additional source code. It works properly.
But now I want to add a logout feature. I've added an endpoint but it doesn't work. I tried to do the following:
#RequestMapping("/logout")
public void logout(HttpServletRequest request, HttpServletResponse response) throws ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
authentication.setAuthenticated(false);
new SecurityContextLogoutHandler().logout(request,response,authentication);
SecurityContextHolder.clearContext();
request.logout();
request.getSession().invalidate();
}
But I am still logged in and can access / url and it responds to me with the username.
Can you help me fix this issue?
Update
I tried the approach described here https://spring.io/guides/tutorials/spring-boot-oauth2/#_social_login_logout :
#EnableAutoConfiguration
#Configuration
#EnableOAuth2Sso
#Controller
public class ClientApplication extends WebSecurityConfigurerAdapter {
private Logger logger = LoggerFactory.getLogger(ClientApplication.class);
#RequestMapping("/hello")
public String home(Principal user, HttpServletRequest request, HttpServletResponse response, Model model) throws ServletException {
model.addAttribute("name", user.getName());
return "hello";
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.antMatcher("/**")
.authorizeRequests()
.antMatchers( "/login**", "/webjars/**", "/error**").permitAll()
.anyRequest()
.authenticated()
.and().logout().logoutSuccessUrl("/").permitAll()
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// #formatter:on
}
public static void main(String[] args) {
new SpringApplicationBuilder(ClientApplication.class)
.properties("spring.config.name=application").run(args);
}
}
and on FE I wrote:
<script type="text/javascript">
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (settings.type == 'POST' || settings.type == 'PUT'
|| settings.type == 'DELETE') {
if (!(/^http:.*/.test(settings.url) || /^https:.*/
.test(settings.url))) {
// Only send the token to relative URLs i.e. locally.
xhr.setRequestHeader("X-XSRF-TOKEN",
Cookies.get('XSRF-TOKEN'));
}
}
}
});
var logout = function () {
$.post("/client/logout", function () {
$("#user").html('');
$(".unauthenticated").show();
$(".authenticated").hide();
});
return true;
};
$(function() {
$("#logoutButton").on("click", function () {
logout();
});
});
</script>
and
<input type="button" id="logoutButton" value="Logout"/>
But it still doesn't work. It results in the following behavior:
Post http://localhost:9999/client/logout redirects to the http://localhost:9999/client but this page doesn't exist
source code on gitub:
client - https://github.com/gredwhite/logour_social-auth-client (use localhost:9999/client/hello url)
server - https://github.com/gredwhite/logout_social-auth-server
You can delete the refresh token as well as access token from database to save space.
#PostMapping("/oauth/logout")
public ResponseEntity<String> revoke(HttpServletRequest request) {
try {
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.contains("Bearer")) {
String tokenValue = authorization.replace("Bearer", "").trim();
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
tokenStore.removeAccessToken(accessToken);
//OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(tokenValue);
OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
tokenStore.removeRefreshToken(refreshToken);
}
} catch (Exception e) {
return ResponseEntity.badRequest().body("Invalid access token");
}
return ResponseEntity.ok().body("Access token invalidated successfully");
}
The URL to logout will be : http://localhost:9999/oauth/logout
Also, pass the access token in the Authorization header, as
Authorization: Bearer 0cb72897-c4f7-4f01-aed9-2f3f79a75484
where, 0cb72897-c4f7-4f01-aed9-2f3f79a75484 is the access token.
Since, its Spring security, don't forget to bypass /oauth/logout url from authorize access, as
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/hello", "/oauth/logout");
}
Hope, it will solve your logout problem in Springboot2+Oauth2. Its working for me.
Add following code snippet to your ClientApplication class. This will also clear your session details.
Replace below code with the configure method of your web security adapter class.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers( "/login**", "/webjars/**", "/error**").permitAll()
.anyRequest()
.authenticated()
.and().logout().invalidateHttpSession(true)
.clearAuthentication(true).logoutSuccessUrl("/login?logout").deleteCookies("JSESSIONID").permitAll().and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
You probably want to use the Spring Security built-in support for the /logout endpoint which will do the right thing (clear the session and invalidate the cookie). To configure the endpoint extend the existing configure() method in our WebSecurityConfigurer:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.and().logout().logoutSuccessUrl("/").permitAll();
}
You can change Post to
Get http://localhost:9999/client/logout
it works for me
Try to add logout url to your security configuration.
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.permitAll();

Categories