Handling SAML Redirects on AJAX Requests - angularjs

I have several AngularJS apps all using Spring/Java and SAML 2.0 for SSO (leveraging the Spring Security SAML extension). My SSO id provider is OpenAM and everything is working pretty well. However, I am running into a situation when a user does a global logout from within one application but has other tabs open. Since these are single page web apps, a lot of functionality may still be usable in the orphaned tabs UNTIL, the user does something to invoke an ajax request. Of course, these AJAX requests get intercepted by the Spring Security SAML filters and triggers an authentication attempt via a REDIRECT to the OpenAM login URL. Of course, this wreaks havoc in the browser since redirects to another domain aren't allowed on AJAX requests. Furthermore, I can't really do anything with Angular's $http interceptors as the requests are 'canceled' and no quality information is available in the $http error callback function (such as a convenient 401/403 status code). All I know is that the request failed.
I don't want to assume that all bad $http requests are due to authentication problems (and do a $window.location.reload()) as there could be legitimate reasons for failure. My preference is to suppress the Spring Security redirect (to OpenAM login page) for ajax requests and, instead, send back a 401/403 status code. This would allow me to handle the error in the $http interceptor and do a full page load if it is an authentication failure, thus elegantly redirecting to the login page as if they were going to the site for the first time.
Any ideas for how to accomplish this?

The bean responsible for initialization of authentication and decision to return an HTTP error, perform a redirect, ... is an instance of AuthenticationEntryPoint. To change its behavior you can either:
customize the current SAMLEntryPoint (extend the commence method) and override the default behavior in case request is an AJAX call from Angular, so it returns an HTTP error instead of performing redirect to IDP
or define another security:http element in your Spring context (before the current one) which only covers your AJAX requests (e.g. with attribute pattern="/api/**") and uses an entry point which behaves in the way you want (see Http403ForbiddenEntryPoint)

Referring to a possible implementation of Vladimir's first bullet - taken from https://www.jasha.eu/blogposts/2015/10/saml-authentication-angularjs-spring-security.html
public class XhrSamlEntryPoint extends SAMLEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
if (isXmlHttpRequest(request) && e instanceof InsufficientAuthenticationException) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
return;
}
super.commence(request, response, e);
}
private boolean isXmlHttpRequest(HttpServletRequest request) {
return "XMLHttpRequest".equalsIgnoreCase(request.getHeader("X-Requested-With"));
}
}
Keep in mind that X-Requested-With is not a mandatory header so the detection is not bullet-proof according to this answer. In my case since the backend was used with a SPA frontend, I removed the check of ajax call altogether.

Related

Solving CORS issues when calling fetch from React App [duplicate]

This question already has an answer here:
CORS Unauthorized 401 error when calling spring rest api
(1 answer)
Closed 3 years ago.
I'm trying to send http requests from my React client app to a RestfulApi (built with Spring), but I'm facing CORS issues.
As I understand it to solve cors issue there are several ways, 2 of them that I know:
1) Install a CORS extension on your browser (your browser will be exposed to security risks, but if it's only during development and it can be enabled/disabled then I can live with it)
2) Allow headers from your server rest API- I found a spring annotation called #CrossOrigin(origins = "", allowedHeaders = "")
https://howtodoinjava.com/spring5/webmvc/spring-mvc-cors-configuration/
Using the first approach:
First call that I perform is GET login request and it works, but I'm getting back an empty response with empty headers!
Second call is another GET request to get some projects details, but I'm getting 401!
From some investigation I understood that there is a need to send the browser sessionId (stored in cookies) and pass it to the HTTP request as param in order to be authorized... but didn't manage yet making it works.
I reached this page:
https://www.baeldung.com/spring-security-cors-preflight
Adding another class to my rest server app didn't help as well:
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.cors();
}
}
Install a CORS extension on your browser (your browser will be exposed to security risks, but if it's only during development and it can be enabled/disabled then I can live with it)
CORS extensions tend to just inject Access-Control-Allow-* headers into responses.
They don't do everything else that needs to be done to enable CORS.
In particular, they tend not to handle the preflight requests that need before credentials can be sent in a request. So:
the browser sends a preflight request asking for permission to make a request with credentials
the server responds with an Unauthorized error because the credentials weren't sent
the extension injects Access-Control-Allow-* headers
the browser denied access to the response to the JS because the response had an Unauthorised status (the Access-Control-Allow-* headers not being sufficient to override that)
Implement a real solution when you need CORS. Browser extensions are a waste of time because the will need replacing with real solutions in the end and only work in a subset of cases in the first place.

AngularJS: How to parse response of 302

I am implementing a Login page with AngularJS to authenticate against LDAP server. The authentication at the back end is done by Spring Security. Basically username and password are sent to server via a post request which is handled by Spring without an explicit handler.
When I submit the login form, the post request returns 302 and redirect to another url based on whether credentials are valid or not. If the password is correct, it will initiate GET request to "http://localhost:8080/". If password is wrong, it redirects to "http://localhost:8080/login?error". This is known behavior of Spring Security. According to Dave Syer's article, "the Spring Security default behaviour is to send a 302 on success and failure, and Angular will follow the redirect, so we would have to actually parse the response from that". In Dave's tutorial, he used a helper function to verify the authentication in the general case. I don't think it would work for LDAP authentication though.
I found another very similar post Spring Boot and Security with custom AngularJS Login page. Although it does not have an official answer, based on the last comment, it seems modifying paths in .antMatchers in Java config may have resolved the issue. However I played with my security config (see below) back and forth and it did not seem to help.
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().and()
.addFilterBefore(new CORSFilter(), ChannelProcessingFilter.class)
.csrf().disable()
.authorizeRequests()
.antMatchers("/login/", "/login","login/")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin();
}
Although 302 does not produce any response, client somehow knows which url to redirect to based on credential's validity. My understanding is that the server must have told client if authentication succeeds or not in a "secret" way. If I capture and parse this hidden information before client sends GET request, I could make the authentication work with Angular. Something like this (partially pseudo code):
app.controller("LoginCtrl", ['$scope', '$location',
function($scope, $location){
$scope.authenticate = function() {
loginFactory.login($scope.username, $scope.password) {
// pseudo code starts
if (redirect.path == 'localhost') {
$location.path('/main');
}
else {
$location.path('/loginfail');
console.log("Login failed");
}
}
}]);
The question is how to diagnose the 302 and identify the nature of the response before redirect happens? Worst case scenario, I could let the redirect to start, and grab response.path from the GET request and decide whether login is successful. But I try not to go down that path. Need suggestion badly.
After hours of google search, I found a way to suppress the 302 and thus avoid redirects based on this article. The resolution is to inject a custom authentication success handler in the form login filter and SimpleUrlAuthenticationFailureHandler to handle failed authentication.

Spring Security CSRF protection of REST backend - transfer Synchronizer Token Pattern to the client

I read a lot about Spring Securitys CSRF protection, but i still struggle a little bit. Now the documentation is great as usual, but it's completely based on the idea that you render html code on the server and are able to add a hidden field to every form. Now since i use AngularJS and JavaScript to call the backend this is not really an option.
So what is the best way to actually get the Token to the client in this case (Rest Backend / AngularJS frontend)? AngularJS seems to have built in support for CSRF in $resource and expects a Cookie called "XSRF-TOKEN" to retrieve the Token and send it as http header "X-XSRF-TOKEN" in further requests. So every request will contain the http header, as well as the cookie. Now on server side i could read the header and compare it to the Token i stored in the session.
The problem i have with this, it that it seems a bit complicated. Since the login itself has to be protected it would require creating a temporary session, just for the CSRF token. Is this really necessary?
Maybe this is just a stupid question, but why can't i just create a random-token on client side and set it as HTTP header and cookie on client side. This would be similar to "OWASP double submit cookie", but generate the Token on client-side. That way the server would not require to have a session before login, since he could just compare the 2 submitted tokens. Now while the attacker could send the HTTP header, he would per same-origin-policy have no way of reading or setting the cookie and could not get a match as long as the number is practically unguessable.
Now instinctly generating a secure token on client side seems dangerous to me and i guess i coul avoid it.. but WHY? I feel like i am missed something, surely there is a good reason why SpringSecurity stores the token in the session, right?
Please enlighten me :)
I ended up using spring-security-csrf-token-interceptor-extended, which reads the CSRF-Token from the http-header "X-CSRF-TOKEN" (name is configurable) and sends it as http-header on further requests.
Now the only thing i had to to was getting Spring-Security to send the Token as HTTP Header (since i don't render html code on serverside and therefor can't add it as a hidden field).
<security:http ....
<security:custom-filter ref="csrfTokenResponseHeaderBindingFilter" after="CSRF_FILTER"/>
....
</security:http>
The filter basically runs after the normal CSRF_FILTER and reads the "_csrf" request-attribute (which is put there by CSRF_FILTER) and sets it as header "X-CSRF-TOKEN"
public class CsrfTokenResponseHeaderBindingFilter extends OncePerRequestFilter {
protected static final String REQUEST_ATTRIBUTE_NAME = "_csrf";
protected static final String RESPONSE_TOKEN_NAME = "X-CSRF-TOKEN";
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, javax.servlet.FilterChain filterChain) throws ServletException, IOException {
CsrfToken token = (CsrfToken) request.getAttribute(REQUEST_ATTRIBUTE_NAME);
if (token != null) {
response.setHeader(RESPONSE_TOKEN_NAME, token.getToken());
}
filterChain.doFilter(request, response);
}
}

Restlet CorsFilter with ChallengeAuthenticator

I'm building a RESTful API with the Restlet framework and need it to work with cross domain calls (CORS) as well as basic authentication.
At the moment I'm using the CorsFilter which does the job of making my webservice support CORS requests. But, when I try to use this with a simple ChallengeAuthenticator with HTTP Basic Authentication it won't work as I want it to (from a web site).
When I access the webservice directly via Chrome it works as intended, but when I try it in a small web application written in angularjs (jquery/javascript) and try to access the webservice it does not.
Basically what happens is that when a OPTIONS request is sent to my webservice it will not respond with the headers: 'Access-Control-Allow-Origin', 'Access-Control-Allow-Credentials', etc. as it should. Instead it is sending a respond with HTTP status code 401 saying that the authentication failed.. Is this because the authenticator is overriding the CorsFilter somehow?
My createInboundRoot method can be seen below.
#Override
public Restlet createInboundRoot() {
ChallengeAuthenticator authenticator = createAuthenticator();
RoleAuthorizer authorizer = createRoleAuthorizer();
Router router = new Router(getContext());
router.attach("/items", ItemsServerResource.class);
router.attach("/items/", ItemsServerResource.class);
Router baseRouter = new Router(getContext());
authorizer.setNext(ItemServerResource.class);
authenticator.setNext(baseRouter);
baseRouter.attach("/items/{itemID}", authorizer);
baseRouter.attach("", router);
// router.attach("/items/{itemID}", ItemServerResource.class);
CorsFilter corsFilter = new CorsFilter(getContext());
corsFilter.setNext(authenticator);
corsFilter.setAllowedOrigins(new HashSet(Arrays.asList("*")));
corsFilter.setAllowedCredentials(true);
return corsFilter;
}
(The authorizer and authenticator code is taken from the "official" restlet guide for authorization and authentication)
I've tried alot of changes to my code but none which given me any luck. But I noticed that when setting the argument "optional" in ChallengeAuthenticator to true (which "Indicates if the authentication success is optional") the CorsFilter does its job, but obviously the ChallengeAuthenticator does not care about authenticating the client and lets anything use the protected resources..
Has anyone had a similar problem? Or have you solved this (CORS + Authentication in Restlet) in any other way?
Thanks in advance!
I think that it's a bug of the Restlet CORS filter. As a matter of fact, the filter uses the method afterHandle to set the CORS headers. See the source code: https://github.com/restlet/restlet-framework-java/blob/4e8f0414b4f5ea733fcc30dd19944fd1e104bf74/modules/org.restlet/src/org/restlet/engine/application/CorsFilter.java#L119.
This means that the CORS processing is done after executing the whole processing chain (authentication, ...). So if your authentication failed, you will have a status code 401. It's actually the case since CORS preflighted requests don't send authentication hints.
For more details about using CORS with Restlet, you could have a look at this link: https://templth.wordpress.com/2014/11/12/understanding-and-using-cors/. This can provide you a workaround until this bug was fixed in Restlet itself.
I opened an issue in Github for your problem: https://github.com/restlet/restlet-framework-java/issues/1019.
Hope it helps,
Thierry
The CorsService (in 2.3.1 coming tomorrow) contains also a skippingResourceForCorsOptions property, that answers directly the Options request without transmitting the request to the underlying filters and server resources.

How to secure an AngularJS application that does not use authentication?

I have an AngularJS application (using ASP.NET Web API as the backend) that does not require the user to authenticate (the application has no login). I want to make sure that the REST API methods the AngularJS application invokes can only be invoked from the application. I obviously cannot use token based authentication for that.
If doing nothing special the REST API methods can be invoked freely using the browsers address bar or by writing a desktop application that invokes them. The same-origin policy is only regarded if a browser invokes an API method by a HTML page coming from a site having another origin. The REST API is therefore open to the public and easily hackable.
I wonder what I could do to securely restrict the access to the REST API. Any ideas or experience?
Edit 1:
I found an easy solution for my problem: I just check if the host of the URL referrer is the same as the host of the requested URL. Using ASP.NET Web API the code the REST API actions use is:
private bool ApiCallIsAllowed()
{
var request = HttpContext.Current.Request;
return (request.UrlReferrer != null &&
request.UrlReferrer.Host == request.Url.Host);
}
I am just not 100% sure if I always get the URL referrer.
Edit 2: According to this question the URL referrer ist not reliable. Bummer.
use ssh - that's obvious :)
login process should generate token - write it as a cooke - every http request will use it in header
prepare rest interceptor that will read your token and authorize every request
use some Javascript Obfuscator
Don't forget to invalidate session ;).
you can use spring-security or other framework to simplify this process

Resources