I am trying to authorize all preflight request in (/secure/**) without an authorization header(oauth token in my case). The JwkFilter is used to validate the oauth token passed in the authorization header. Any suggestion, where I am going wrong here.
#Override
protected void configure(HttpSecurity http) throws Exception {
JwtAuthFilter jwtAuthTokenFilter = new JwtAuthFilter(oauthConfig);
jwtAuthTokenFilter.setAuthenticationManager(getAuthManager());
http.cors().and().authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/secure/**")
.permitAll();
http.requiresChannel().anyRequest().requiresSecure().and()
.addFilterBefore(requireProtocolFilter, ChannelProcessingFilter.class).sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().anonymous().disable().csrf().disable()
.antMatcher("/**").authorizeRequests().anyRequest().permitAll().and()
.antMatcher(/secure/**")
.addFilterBefore(jwtAuthTokenFilter, BasicAuthenticationFilter.class).exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint()).and().authorizeRequests().anyRequest()
.authenticated();
}
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("*")
.allowedOrigins("*");
}
};
}
For preflight request with CORS, according to spring, they will execute before your jwtAuthTokenFilter (registered before BasicAuthenticationFilter filter) -> correct
The order was specified here (in spring code):
FilterComparator() {
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
...
put(CorsFilter.class, order.next());
...
put(BasicAuthenticationFilter.class, order.next());
...
}
In CORS, for complex request (like using custom header Authorization header in your case), browser will send preflight request first to know whether the server allow client to access their resource or not before sending actual request.
The CORSFilter will execute like this (in spring code):
public class CorsFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
return;
}
filterChain.doFilter(request, response);
}
}
They will check whether for every preflight request (extends OncePerRequestFilter) comes to server, if processRequest is valid or is preflight request to terminate the chain.
Here is the default processor to check preflight request (in spring code):
public class DefaultCorsProcessor implements CorsProcessor {
#Override
public boolean processRequest(#Nullable CorsConfiguration config, HttpServletRequest request,
HttpServletResponse response) throws IOException {
...
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
rejectRequest(new ServletServerHttpResponse(response));
return false;
}
else {
return true;
}
}
return handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);
}
In your case, I think you are missing configuring for enabling CORS.
So the server reject the client request (by sending HttpStatus.FORBIDDEN code), so that the browser don't send actual request to the server.
And your JwtAuthTokenFilter has no chance to execute.
You can refer to this post for configuring cors. Hope it helps
Adding the below snippet in to the jwkAuthFilter did the trick.
if (CorsUtils.isPreFlightRequest(request)) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
Related
I am building a microservice using Spring Cloud Gateway and OAuth2 Resource Server. The app aims at redirecting to other microservices after doing the security part. I am trying to setup a filter before AnonymousAuthenticationFilter and handle my custom exception from there but however the custom exception filter is never being invoked. Following the security config I have in the app:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().cors().disable()
.httpBasic().disable()
.formLogin().disable()
.addFilterBefore(customExceptionHandler, AnonymousAuthenticationFilter.class)
.authorizeRequests( auth -> auth.antMatchers(AUTH_WHITELIST).permitAll()
.antMatchers("/**").authenticated())
.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.jwt())
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
}
In my customExceptionHandler, I have the following code:
public class CustomExceptionHandler extends OncePerRequestFilter {
#Autowired
#Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (Exception e) {
log.error("Spring Security Filter Chain Exception:", e);
resolver.resolveException(request, response, null, e);
}
}
}
Also following is my build.gradle:
// Spring Boot
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Spring Cloud
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
I also have an #ExceptionAdvice class that handles all the exceptions. However, if I pass in an expired JWT or any other error scenario to the service, I always get handled by the following error message in my WWW-Authenticate header:
Bearer error="invalid_token", error_description="Jwt expired at 2022-06-16T19:58:09Z", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
How do I throw a custom POJO instead of this message?
This error is coming from BearerTokenAuthenticationEntryPoint, so to override the behavior you can just easily provide a custom entryPoint
.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.jwt().and().authenticationEntryPoint(myCustomEntryPoint))
I have an application with multiple authentication types (i.e. Basic and a special Preauthorized login). I am attempting to add a SAML2 RelyingParty registration in my security configuration, where I am attempting to change the default path from:
/login/saml2/sso/{registrationId}
to
/auth/saml2/{registrationId}
So, I have the following setup:
public RelyingPartyRegistration provder1RelyingPartyRegistration() {
RelyingPartyRegistration registration = RelyingPartyRegistrations
.fromMetadataLocation("classpath:provider1/metadata.xml")
.registrationId("provider1")
.assertionConsumerServiceLocation("{baseUrl}/auth/saml2/{registrationId}")
.build();
return registration;
}
// #Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
Collection<RelyingPartyRegistration> registrations = Collections.unmodifiableList(Arrays.asList(provider1RelyingPartyRegistration()));
InMemoryRelyingPartyRegistrationRepository repository = new InMemoryRelyingPartyRegistrationRepository(registrations);
return repository;
}
// fluff
#Override
protected void configure(HttpSecurity http) throws Exception {
final RequestMatcher filterRequestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher("/auth/basic"),
new AntPathRequestMatcher("/auth/preauth")
);
ApplicationAuthenticationProcessingFilter filter = new ApplicationAuthenticationProcessingFilter(filterRequestMatcher, authenticationManagerBean());
filter.setAuthenticationSuccessHandler(successHandler());
filter.setAuthenticationFailureHandler(failureHandler());
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.and()
.addFilterAfter(filter, LogoutFilter.class)
// fluff
.and()
.saml2Login()
.relyingPartyRegistrationRepository(relyingPartyRegistrationRepository())
.loginProcessingUrl("/auth/saml2/{registrationId}")
;
}
Unfortunately, I get this:
14 Dec 10:55:34 WARN [https-openssl-nio-127.0.0.1-444-exec-2] (DispatcherServlet.java:1278) - No mapping for POST /svc/auth/saml2/provider1
Can anyone tell me what I'm doing wrong trying to change that path? My application does NOT use Spring Boot, so I'm stuck with manual configuration.
EDIT
Some debugging has led to this hitting this line in the Saml2LoginConfigurer:
Map<String, String> providerUrlMap = getIdentityProviderUrlMap(
this.authenticationRequestEndpoint.filterProcessingUrl, this.relyingPartyRegistrationRepository);
Somehow, there's a default authenticationRequestEndpoint (since I didn't define one) setting the filterProcessingUrl to a value of /saml2/authenticate/{registrationId}. So, how do I override this?
The loginProcessingUrl is called by the asserting party after the authentication succeeds, which contains in the request the SAMLResponse parameter.
What you are trying to change is the URL to process an authentication request (create the SAMLRequest and send to the asserting party), this is done by the Saml2WebSsoAuthenticationRequestFilter class. To change the redirectMatcher you have to provide an ObjectPostProcessor, see this issue.
ObjectPostProcessor<Saml2WebSsoAuthenticationRequestFilter> processor = new ObjectPostProcessor<>() {
#Override
public <O extends Saml2WebSsoAuthenticationRequestFilter> O postProcess(O filter) {
filter.setRedirectMatcher(new AntPathRequestMatcher("/my/custom/url"));
return filter;
}
};
http.saml2Login().addObjectPostProcessor(processor);
Take a look at SAML 2.0 Login Overview for more detail about the flow.
I'm writing a filter that would intercept an Restful API call , extract a Bearer token and make a call to an Authorization Server for validation.
I couldn't find one in Spring Boot that does it out of the box, but I'm sure there is a cleaner way to do this.
here is what I have (pseudo code):
public class SOOTokenValidationFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String xAuth = request.getHeader("Authorization");
// validate the value in xAuth
if(isValid(xAuth) == false){
throw new SecurityException();
}
// Create our Authentication and set it in Spring
Authentication auth = new Authentication ();
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
}
private boolean isValid (String token){
// make a call to SSO passing the access token and
// return true if validated
return true;
}
}
Lessons learned, Spring Security Oauth2 documentation is woefully inadequate, forget about trying to use the framework without fully combing through the source code. On the flip side the code is well written and easy to follow kudos to Dave Syer.
Here is my config:
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/")
.permitAll()
.and()
.addFilterBefore(getOAuth2AuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling();
}
Here is my getOAuth2AuthenticationProcessingFilter method:
private OAuth2AuthenticationProcessingFilter getOAuth2AuthenticationProcessingFilter() {
// configure token Extractor
BearerTokenExtractor tokenExtractor = new BearerTokenExtractor();
// configure Auth manager
OAuth2AuthenticationManager manager = new OAuth2AuthenticationManager();
// configure RemoteTokenServices with your client Id and auth server endpoint
manager.setTokenServices(remoteTokenServices);
OAuth2AuthenticationProcessingFilter filter = new OAuth2AuthenticationProcessingFilter();
filter.setTokenExtractor(tokenExtractor);
filter.setAuthenticationManager(manager);
return filter;
}
Referring to the logout flow in oauth2 spring-guides project, once the the user has authenticated using user/password for the first time, the credentials are not asked next time after logout.
How can I ensure that username/password are asked every time after a logout.
This is what I am trying to implement:-
OAuth2 server issuing JWT token using "authorization_code" grant type
with auto approval. This has html/angularjs form to collect
username/password.
UI/Webfront - Uses #EnableSSO. ALL its endpoints are authenticated
i.e it does not have any unauthorized landing page/ui/link that user
clicks to go to /uaa server. So hitting http://localhost:8080
instantly redirects you to http://localhost:9999/uaa and presents
custom form to collect username/password.
Resource server - Uses #EnableResourceServer. Plain & simple REST api.
With the above approach I am not able to workout the logout flow. HTTP POST /logout to the UI application clears the session/auth in UI application but the users gets logged in again automatically ( as I have opted for auto approval for all scopes) without being asked for username password again.
Looking at logs and networks calls, it looks like that all the "oauth dance" happens all over again successfully without user being asked for username/password again and seems like the auth server remembers last auth token issued for a client ( using org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices? ).
How can I tell auth server to ask for username/password every time it is requested for code/token - stateless.
Or what is the best way to implement logout in my given scenario.
( To recreate somewhat near to my requirements, remove permitAll() part from the UiApplication and configure autoApproval in auth server of the mentioned boot project.)
github issue
I also faced the error as you described and I saw a solution from question
Spring Boot OAuth2 Single Sign Off. I don't mean this is the only and global truth solution.
But in the scenario,
authentication server has login form and you'd authenticated from it
browser still maintain the session with authentication server
after you have finished logout process (revoke tokens,remove cookies...)
and try to re-login again
authentication server do not send login form and automatically sign in
You need to remove authentication informations from authentication server's session as this answer described.
Below snippets are how did I configure for solution
Client (UI Application in your case) application's WebSecurityConfig
...
#Value("${auth-server}/ssoLogout")
private String logoutUrl;
#Autowired
private CustomLogoutHandler logoutHandler;
...
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl(logoutUrl)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.addLogoutHandler(logoutHandler)
.and()
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
// #formatter:on
}
Custom logout handler for client application
#Component
public class CustomLogoutHandler implements LogoutHandler {
private static Logger logger = Logger.getLogger(CustomLogoutHandler.class);
#Value("${auth-server}/invalidateTokens")
private String logoutUrl;
#Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
logger.debug("Excution CustomLogoutHandler for " + authentication.getName());
Object details = authentication.getDetails();
if (details.getClass().isAssignableFrom(OAuth2AuthenticationDetails.class)) {
String accessToken = ((OAuth2AuthenticationDetails) details).getTokenValue();
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("access_token", accessToken);
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "bearer " + accessToken);
HttpEntity<Object> entity = new HttpEntity<>(params, headers);
HttpMessageConverter<?> formHttpMessageConverter = new FormHttpMessageConverter();
HttpMessageConverter<?> stringHttpMessageConverternew = new StringHttpMessageConverter();
restTemplate.setMessageConverters(Arrays.asList(new HttpMessageConverter[] { formHttpMessageConverter, stringHttpMessageConverternew }));
try {
ResponseEntity<String> serverResponse = restTemplate.exchange(logoutUrl, HttpMethod.POST, entity, String.class);
logger.debug("Server Response : ==> " + serverResponse);
} catch (HttpClientErrorException e) {
logger.error("HttpClientErrorException invalidating token with SSO authorization server. response.status code: " + e.getStatusCode() + ", server URL: " + logoutUrl);
}
}
authentication.setAuthenticated(false);
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
new SecurityContextLogoutHandler().logout(request, response, auth);
}
}
I used JDBC tokenStore, so I need to revoke tokens.At the authentication server side, I added a controller to handle logout processes
#Controller
public class AuthenticationController {
private static Logger logger = Logger.getLogger(AuthenticationController.class);
#Resource(name = "tokenStore")
private TokenStore tokenStore;
#Resource(name = "approvalStore")
private ApprovalStore approvalStore;
#RequestMapping(value = "/invalidateTokens", method = RequestMethod.POST)
public #ResponseBody Map<String, String> revokeAccessToken(HttpServletRequest request, HttpServletResponse response, #RequestParam(name = "access_token") String accessToken, Authentication authentication) {
if (authentication instanceof OAuth2Authentication) {
logger.info("Revoking Approvals ==> " + accessToken);
OAuth2Authentication auth = (OAuth2Authentication) authentication;
String clientId = auth.getOAuth2Request().getClientId();
Authentication user = auth.getUserAuthentication();
if (user != null) {
Collection<Approval> approvals = new ArrayList<Approval>();
for (String scope : auth.getOAuth2Request().getScope()) {
approvals.add(new Approval(user.getName(), clientId, scope, new Date(), ApprovalStatus.APPROVED));
}
approvalStore.revokeApprovals(approvals);
}
}
logger.info("Invalidating access token :- " + accessToken);
OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(accessToken);
if (oAuth2AccessToken != null) {
if (tokenStore instanceof JdbcTokenStore) {
logger.info("Invalidating Refresh Token :- " + oAuth2AccessToken.getRefreshToken().getValue());
((JdbcTokenStore) tokenStore).removeRefreshToken(oAuth2AccessToken.getRefreshToken());
tokenStore.removeAccessToken(oAuth2AccessToken);
}
}
Map<String, String> ret = new HashMap<>();
ret.put("removed_access_token", accessToken);
return ret;
}
#GetMapping("/ssoLogout")
public void exit(HttpServletRequest request, HttpServletResponse response) throws IOException {
new SecurityContextLogoutHandler().logout(request, null, null);
// my authorization server's login form can save with remember-me cookie
Cookie cookie = new Cookie("my_rememberme_cookie", null);
cookie.setMaxAge(0);
cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
response.addCookie(cookie);
response.sendRedirect(request.getHeader("referer"));
}
}
At authorization server's SecurityConfig, you may need to allow this url as
http
.requestMatchers()
.antMatchers(
"/login"
,"/ssoLogout"
,"/oauth/authorize"
,"/oauth/confirm_access");
I hope this may help a little for you.
As you are using JWT tokens, you can not really revoke them.
As a workaround, you can have a logout rest endpoint that would store the timestamp and userid for logout call.
Later, you can compare the logout time with JWT token issue time, and decide wether to allow an api call or not.
I have realized that redirecting to a controller when you logout from your client app and then programmatically logout on your authserver does the trick. This is my configuration on the client app:
#Configuration
#EnableOAuth2Sso
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Value("${auth-server}/exit")
private String logoutUrl;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.logoutSuccessUrl(logoutUrl)
.and().authorizeRequests().anyRequest().authenticated();
}
}
and this is my configuration on my authserver (is just a controller handling the /exit endpoint):
#Controller
public class LogoutController {
public LogoutController() {
}
#RequestMapping({"/exit"})
public void exit(HttpServletRequest request, HttpServletResponse response) {
(new SecurityContextLogoutHandler()).logout(request, null, null);
try {
response.sendRedirect(request.getHeader("referer"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
Here is a sample app that shows the full implementation using JWT. Check it out and let us know if it helps you.
I am using Spring security in my application and configuration is mentioned below.
When User is logged out from system and click in again login then I want to display login page.
#Component
#EnableOAuth2Sso
public static class LoginConfigurer extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
RequestMatcher csrfRequestMatcher = new RequestMatcher() {
// Enabled CSFR protection on the following urls
private AntPathRequestMatcher[] requestMatchers = { new AntPathRequestMatcher("/dashboard/logout") };
#Override
public boolean matches(HttpServletRequest request) {
// If the request match one url the CSFR protection will be
// enabled
for (AntPathRequestMatcher rm : requestMatchers) {
if (rm.matches(request)) {
return true;
}
}
return false;
} // method matches
};
http.csrf().requireCsrfProtectionMatcher(csrfRequestMatcher).csrfTokenRepository(csrfTokenRepository())
.and().antMatcher("/dashboard/**").authorizeRequests().anyRequest()
.hasAnyRole("AUTHENTICATED_USER", "ANONYMOUS").and().sessionManagement().sessionFixation()
.migrateSession().and()
// .csrfTokenRepository(csrfTokenRepository()).and()
// .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class)
.logout().invalidateHttpSession(true).logoutUrl("/dashboard/logout").deleteCookies(new String[]{"XSRF-TOKEN","JSESSIONID","remember-me"})
.logoutSuccessUrl("/").permitAll().and().rememberMe().and()
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);
}
The case you mentioned mostly happens if you use a cookie for authentication as a token and the deleteCookie method is not deleting your authentication cookies correctly.
The best way to identify this is follow :
1.Clear all cookies and login and then observe the cookie name and value and path in inspect element of web browser.
2.Logout and then observe the inspect element request and response with cookies.
3.Observe the path of the cookie carefully.
It is important to observe the path of the cookie is because of the path of the cookie at its creation time is different than its deletion time,it will not get deleted.It has to be exactly the same.
When you call .deleteCookies(...),then internally it calls the CookieClearingLogoutHandler(spring boot security internal class) mentioned as below :
public final class CookieClearingLogoutHandler implements LogoutHandler {
private final List<String> cookiesToClear;
public CookieClearingLogoutHandler(String... cookiesToClear) {
Assert.notNull(cookiesToClear, "List of cookies cannot be null");
this.cookiesToClear = Arrays.asList(cookiesToClear);
}
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
for (String cookieName : cookiesToClear) {
Cookie cookie = new Cookie(cookieName, null);
String cookiePath = request.getContextPath();
if(!StringUtils.hasLength(cookiePath)) {
cookiePath = "/";
}
cookie.setPath(cookiePath);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
}
}
Observe the cookie setPath, you would need to set the cookie path for the cookie you are using for authentication in the same way mentioned in above code.
Hope this helps.