Custom error page on SPNEGO authentication failure - java

I have a Spring MVC REST endpoint which I successfully configured to be secured by Kerberos as recommended. On successful authentication everything works. The problem is when it comes to custom 401 error page.
I have it configured as (I'm spring-boot 1.3.5) as follows:
#Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
return container -> container.addErrorPages(new ErrorPage(HttpStatus.UNAUTHORIZED, "/error/401.html"));
}
This works nicely which I can confirm by switching to e.g. Basic auth and providing wrong credentials.
When back with Kerberos - if I access my secured endpoint with kinit in place everything works and in curl I see the detailed requests:
curl -v -u : --negotiate http://my-enpoint:8080/
> GET / HTTP/1.1
> Host: ...:8080
> User-Agent: curl/7.43.0
> Accept: */*
< HTTP/1.1 401 Unauthorized
< ...
< WWW-Authenticate: Negotiate
> GET / HTTP/1.1
> Host: ...:8080
> Authorization: Negotiate YIIH7 ...
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< ...
Now if I do kdestroy and do the curl again:
curl -v -u : --negotiate http://my-enpoint:8080/
> GET / HTTP/1.1
> Host:...8080
> User-Agent: curl/7.43.0
> Accept: */*
< HTTP/1.1 401 Unauthorized
< ...
< WWW-Authenticate: Negotiate
... and that's it. In this case spring returns 401 as expected response which is part of the handshake and therefore no error page is sent.
And here comes my two questions:
How can I return the 401 error page when the thing dies in the middle of the handshake?
How could spring possibly fallback to any other authentication as fallback (form, basic) if it tries to negotiate but there is no response from the client at all?

So back with my investigation ...
This behaviour I observed is expected. The clients (browser, curl) doesn't continue with authentication if they can't authenticate. The key to provide custom page is the SpnegoEntrypoint. It allows to specify forwardUrl in it and it's javadoc says:
Instantiates a new spnego entry point. This constructor enables
security configuration to use SPNEGO in combination with login form as
fallback for clients that do not support this kind of authentication.
The point is the forward url any resource which will be included in the first 401 response. You can include any page, not just form login. In my case I'm including custom 401 error page because I don't do any authentication fallback.
#Bean
public SpnegoEntryPoint spnegoEntryPoint() {
return new SpnegoEntryPoint("/error/401.html");
}
And then the communication looks like client sends GET and get's back 401 response with my custom error page in body. If the client is able to negotiate it ignores the response body completely and resubmits the request with appropriate token. If it can't authenticate it displays whatever it gets back - the custom error page.

As a response to your first question:
I had the same use case, but chose an alternative solution in which I extended the SpnegoEntryPoint with one that added a small JSON body to the response to be consumed by REST clients:
public class JsonSpnegoEntryPoint extends SpnegoEntryPoint {
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex)
throws IOException, ServletException {
response.getWriter().write("{\"message\": \"Spnego negotiate, expecting to be called with negotiate-flags set\"}");
super.commence(request, response, ex);
}
}

Related

Authorization failed while wget azure REST API

I am trying to collect azure metrics using REST api. I have a free subscription to azure account.
I am using the following wget to get the json message.
wget https://management.azure.com/subscriptions/XXXXXXX/resourceGroups/RG_SOUTH_INDIA/providers/Microsoft.Compute/virtualMachineScaleSets/linuxscal/metrics?api-version=2014-04-01
XXXXXXX- is my subscription id.
I get the following Error message.
Resolving management.azure.com... 13.67.231.219
Connecting to management.azure.com|13.67.231.219|:443... connected.
HTTP request sent, awaiting response... 401 Unauthorized
Authorization failed.
What is wrong with my subscription/Authorization?!!
Thanks in Advance for your help guys!! Am Stuck!!
You need to include an Authorization header with a Bearer token in your call:
GET /subscriptions?api-version=2015-01-01 HTTP/1.1
Host: management.azure.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
Take a look at armclient, since you really don't want to do all this by hand (or by curl):
https://www.npmjs.com/package/armclient
// ES6
import ArmClient, { clientCredentials } from 'armclient';
const client = ArmClient({
subscriptionId: '111111-2222-3333333',
auth: clientCredentials({
tenantId: '444444-555555-666666666',
clientId: '777777-888888-999999999',
clientSecret: 'aaaabbbbbccccc' // or servicePrincipalPassword
})
});
Your /metrics call becomes:
client.get('/resourceGroups/RG_SOUTH_INDIA/providers/Microsoft.Compute/virtualMachineScaleSets/linuxscal/metrics', { 'api-version': '2014-04-01' })
.then((res) => {
console.log(res.body);
console.log(res.headers);
})
.catch((err) => {
console.log(err);
});
By specifying the option --user and --ask-password wget will ask for the credentials. Below is an example. Change the username and link to your needs
wget --user=username --ask-password https://YOUR_URL

java.net.HttpRetryException: cannot retry due to server authentication, in streaming mode

We have two parts in our app:
Server - provide REST services
Client - consume them via Spring restTemplate
In addition to the HTTP status our server returns an HTTP body with JSON that describe error in detail.
So, I've added custom Error handler to restTemplate to treat some error coded as non errors - it helps parse HTTP body very well.
But I get an exception via parsing of the HTTP body in the case of an HTTP/1.1 401 Unauthorized.
All other error codes are handled fine(400, 402, etc. )
We are using plain server logic that sends HTTP response in the case of an error, no special rules for different types of an error:
writeErrorToResponse(int status, String errMsg, HttpServletResponse resp) throws IOException {
response.setStatus(status);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
String message = String.format("{\"error\":\"%s\"}", StringUtils.escapeJson(errMsg));
resp.getWriter().println(message);
}
But on client only HTTP/1.1 401 throws exception - "java.net.HttpRetryException: cannot retry due to server authentication, in streaming mode"
I've made some debugging and see that the cause of the problem is code in SimpleClientHttpResponse:
HttpURLConnection.getInputStream()
Tracing with Fiddler have these next responses:
Message is parsed correct on the client:
HTTP/1.1 402 Payment Required
X-Powered-By: Servlet/3.0
Content-Type: application/json
Content-Language: en-GB
Content-Length: 55
Connection: Close
Date: Sat, 25 May 2013 10:10:44 GMT
Server: WebSphere Application Server/8.0
{"error":"I cant find that user. Please try again."}
And message that is cause of exception:
HTTP/1.1 401 Unauthorized
X-Powered-By: Servlet/3.0
Content-Type: application/json
Content-Language: en-GB
Content-Length: 55
Date: Sat, 25 May 2013 11:00:21 GMT
Server: WebSphere Application Server/8.0
{"error":"I cant find that user. Please try again."}
What could be the cause of java.net.HttpRetryException in this situation?
In addition: Some times ago this mechanism worked fine. But since we have changed a lot of code in app.
I faced the same issue when using SimpleClientHttpRequestFactory. Solved it by setting
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setOutputStreaming(false);
return requestFactory;
The problem is due to chunking and subsequent retry mechanism incase of authentication.
You can also disable chunks using HttpClientPolicy
This issue is reported in Spring Framework.
Reference:
https://jira.spring.io/browse/SPR-9367
Copying from Reference:
It is very difficult (impossible) to handle a 401 response in the RestTemplate with default settings. In fact it is possible, but you have to supply an error handler and a request factory. The error handler was obvious, but the problem is that the default request factory uses java.net which can throw HttpRetryException when you try to look at the status code of the response (despite it being obviously available). The solution is to use HttpComponentsClientHttpRequestFactory. E.g.
template.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
template.setErrorHandler(new DefaultResponseErrorHandler() {
public boolean hasError(ClientHttpResponse response) throws IOException {
HttpStatus statusCode = response.getStatusCode();
return statusCode.series() == HttpStatus.Series.SERVER_ERROR;
}
});
HttpComponentsClientHttpRequestFactory requires below dependency. Add below dependency in your POM file:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
I was having the exact same issue using Spring Boot 2.2.4 and a TestRestTemplate. It only failed on 401's. Simply adding the latest version of Http Client (4.5.11) in a test scope fixed the problem. No further configuration was necessary.
I think passing a configured HttpComponentsClientHttpRequestFactory to your RestTemplate would help.
Here you might find the solution on how to do it.
The reason that I used such development solution was to handle the response messages in the body of Http error responses like 401, 404, and so on, and also to let the application be deployed on real-server side environment.
This exception has a quite unexpected impact on Spring OAuth2 token retrieval. For this retrieval the standard implementation uses RestTemplate which is configured with SimpleClientHttpRequestFactory where output streaming set to true. If the 4XX authentication error happens , the standard ResponseErrorHandler tries to read the output stream and receives, of course, the discussed exception, but decides to silently consume it, ignore.
Sometimes, however, OAuth2 authentication server writes important information into the output/error stream which must be read and analyzed.
To make this information available, upon the creation of OAuth2RestTemplate, its AccessTokenProvider should be set to a custom one, let's name it ErrorStreamAwareAccessTokenProvider:
oauth2RestTemplate.setAccessTokenProvider(new ErrorStreamAwareAccessTokenProvider());
and this custom AccessTokenProvider should overwrite, e.g. getRestTemplate method and tune the RestTemplate correspondingly, as the accepted answers advise. For example:
class ErrorStreamAwareAccessTokenProvider extends OAuth2AccessTokenSupport{
// your exact superclass may vary depending on grant type and other things
protected RestOperations getRestTemplate() {
RestTemplate template = super.getRestTemplate();
template.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
// ... also set ResponseErrorHandler if needed
return template;
}
}
When this is done, the standard ResponseErrorHandler could read the output/error stream or, if you want, for example, to throw a custom exception which has the retrieved information stored in it, you could set your custom ResponseErrorHandler in the above overwritten method.

HTTPServletResponse status code is not in HTTP response header

I have pretty simple JAX-RS WS:
#Stateless
#Path("/foo")
public class FooFacadeBean {
#GET
#Produces(MediaType.TEXT_HTML)
public String performFooCall(#Context HttpServletRequest request, #Context HttpServletResponse response) {
response.setStatus(500);
return "Hello";
}
}
After deployment I execute: curl -v localhost:8080/foo,
the result was:
About to connect() to localhost port 8080
Trying 127.0.0.1...
connected
Connected to localhost (127.0.0.1) port 8080
GET /foo HTTP/1.1
User-Agent: curl/7.26.0
Host: localhost:8080
Accept: */
HTTP/1.1 200 OK
X-Powered-By: Servlet/3.0 JSP/2.2 (GlassFish Server Open Source Edition 3.1.2 Java/Sun Microsystems Inc./1.6)
Server: GlassFish Server Open Source Edition 3.1.2
Content-Type: text/html
Transfer-Encoding: chunked
Date: Thu, 26 Jul 2012 13:52:10 GMT
Hello
Connection to host localhost left intact
Closing connection
As you can see status code in HTTP hasn't changed at all, despite of it was set manually.
Why does this happen? I've spent a lot of hours googling without any results.
Such workaround as returning Response object, something like this:
Response.status(404).entity("Hello").build();
works perfectly!
Glassfish uses JERSEY as JAX-RS implementation. I use embedded variant of Glassfish.
Basically, that's not how you should use Jersey.
Here's what's happening:
The method executes, and you interact with the ServletResponse and set the status code to 500. But since you don't write any data, the response isn't committed.
Then the method returns a String value. Since Jersey has no way of knowing if you've interacted with the ServletResponse or not, it behaves normally.
A String method that returns implies a 200 response code (void implies 204, etc). Jersey tries to set the response code to the default, ultimately by calling setStatus(200) on the response instance. Since the response hasn't been committed yet, this isn't a problem, and the response of 500 is changed to 200.
The HttpServletResponse is committed and is sent back to the client.
I'm assuming that what you want to do is return a different status code, but in an exceptional case only. The way to do that is to return a String in the normal case, and then throw a WebApplicationException in the exceptional case.
#GET
#Produces(MediaType.TEXT_HTML)
public String performFooCall() {
if (isNormal()) {
return "Hello";
}
throw new WebApplicationException(Response.status(500).entity("Hello").build());
}
Alternatively, you can throw a standard exception, and control how it is rendered as a response with a separate ExceptionMapper.
Jersey building a new response so setting the response code in your code, is override later.
You have to build a new response in your code and put you string in the response (and change your return value for that), or you can create a filter that will work later on in the filter chaing and change the response status.
look here for more details

Get username and password from HttpServletRequest

I need to get username and password from HttpServletRequest to process the basic authentication. What I have is CXF endpoint and the basic auth interceptor. The HttpServletRequest I get like this:
public void handleMessage(Message message)
throws Fault {
HttpServletRequest request = (HttpServletRequest) message.get(AbstractHTTPDestination.HTTP_REQUEST);
}
When I tried to debug the code and I see that:
request.getAuthType() is null
request.getRemoteUser() is null
The username and password I am sending with the request from Soap UI. So the question is how am I able to get the username and password from the request?
EDIT
Header looks like this:
POST http://localhost:8011/GradIrelandUserRegistration HTTP/1.1
Accept-Encoding: gzip,deflate
Content-Type: text/xml;charset=UTF-8
SOAPAction: ""
Authorization: Basic UGF1bGl1czpQYXVsaXVzMTIz
Content-Length: 3170
Host: localhost:8011
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.1.1 (java 1.5)
They say that Authorization is basic, but then why call request.getAuthType() is returning null?
I was able to figure it out my self. In Soap UI I needed to change the Authentication type to:
Preemptive
and then use:
AuthorizationPolicy policy = (AuthorizationPolicy) message.get(AuthorizationPolicy.class.getName());
From the policy object now I am able to get the username and password.
This may be helpful to you
.
I'm bit new in SOAP data parsing so, if not working then let me know brief problem.
.
Also Check this J-Query Plugin for parsing SOAP services with the database. check this Stack Overflow

How do I connect to a remote URL which requires Spring Security forms authentication (Java)?

I've searched and searched but can't seem to find the answer to what seems like a straightforward authentication scenario.
We have an existing Java web application that uses form-based authorization provided by Spring. We are attempting to access this application via our portal site without challenging the user to enter their credentials (SSO).
The portal has a credential vault and we can successfully access the secrets for the remote web application on the server side. We are using Apache's HTTP Components utility to post the login request to the j_spring_security_check and are successfully authenticating. The response to this post sends back a 302 redirect to the application home page and sets a cookie with a session id.
Now we have to somehow send this authenticated session back to the browser and this is where we are having trouble. Simply redirecting the browser to the home page doesn't work - it redirects us to the login page. Forwarding all of the response headers back to the browser exactly as received on the server-side doesn't work either - still returned to the login page.
So, how do we authenticate server-side and still be able to load the target page client-side?
I am relatively new to this so I apologize if this is a silly question. Any help or advice regarding an alternative approach is appreciated.
Notes:
HttpComponent Client code:
DefaultHttpClient httpclient = new DefaultHttpClient();
try {
// try to get the home page
HttpGet httpget = new HttpGet("http://<host>/<root>/home.action");
HttpResponse httpClientResponse = httpclient.execute(httpget);
HttpEntity entity = httpClientResponse.getEntity();
// check status and close entity stream
System.out.println("Login form get: " + httpClientResponse.getStatusLine());
EntityUtils.consume(entity);
// check cookies
System.out.println("Initial set of cookies:");
List<Cookie> cookies = httpclient.getCookieStore().getCookies();
printCookies(cookies);
/*** Login ***/
HttpPost httppost = new HttpPost("http://<host>/<root>/j_spring_security_check");
// Prepare post parameters
List <NameValuePair> nvps = new ArrayList <NameValuePair>();
nvps.add(new BasicNameValuePair("j_username", getUserFromVault()));
nvps.add(new BasicNameValuePair("j_password", getPasswordFromVault()));
httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
httpClientResponse = httpclient.execute(httppost);
// copy response headers and determine redirect location
Header[] allHeaders = httpClientResponse.getAllHeaders();
System.out.println("Headers: ");
String location = "";
for (Header header : allHeaders) {
System.out.println(header);
if("location".equalsIgnoreCase(header.getName())) location = header.getValue();
response.addHeader(header.getName(), header.getValue());
}
// check response body
entity = httpClientResponse.getEntity();
System.out.println("Response content: " + httpClientResponse.getStatusLine());
System.out.println(EntityUtils.toString(entity)); // always empty
EntityUtils.consume(entity);
// check cookies
System.out.println("Post logon cookies:");
cookies = httpclient.getCookieStore().getCookies();
printCookies(cookies);
// populate redirect information in response
System.out.println("Redirecting to: " + locationHeaderValue);
response.setStatus(httpClientResponse.getStatusLine().getStatusCode()); // 302
// test if server-side get works for home page at this point (it does)
httpget = new HttpGet(location);
httpClientResponse = httpclient.execute(httpget);
entity = httpClientResponse.getEntity();
// print response body (all home content is loaded)
System.out.println("home get: " + httpClientResponse.getStatusLine());
System.out.println("Response content: " + httpClientResponse.getStatusLine());
System.out.println(EntityUtils.toString(entity));
EntityUtils.consume(entity);
} finally {
httpclient.getConnectionManager().shutdown();
}
Headers returned from the successful login on the server side:
HTTP/1.1 302 Found
Date: Wed, 23 Feb 2011 22:09:03 GMT
Server: Apache/2.2.3 (CentOS)
Set-Cookie: JSESSIONID=6F98B0B9A65BA6AFA0472714A4C816E5; Path=<root>
Location: http://<host>/<root>/home.action
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
Via: 1.1 PPWebFilter.<host>:80 (IronPort-WSA/7.0.0-825)
Connection: keep-alive
Headers from the client side request and response:
Request:
GET /<root>/home.action HTTP/1.1
Host: <host>
Connection: keep-alive
Referer: http://localhost:10039/SCMViewer/TestLoginServlet?launchScm=Launch+SCM+servlet
Accept:application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.98 Safari/534.13
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Cookie: JSESSIONID=FC8E823AB1A1545BE8518DB4D097E665
Response (redirect to login):
HTTP/1.1 302 Found
Date: Wed, 23 Feb 2011 22:09:03 GMT
Server: Apache/2.2.3 (CentOS)
Location: http://<host>/<root>/security/login.action
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
Via: 1.1 PPWebFilter.<host>:80 (IronPort-WSA/7.0.0-825)
Connection: keep-alive
As a test, we wrote a bit of a hack that seems to work, but is too insecure to be viable:
Embedded a form on the jsp which will post the login credentials directly to the remote site's j_spring_security_check.
Wrote a servlet method to retrieve the credentials from the vault.
Filled the credentials on the client side into hidden form fields and submitted the form via javascript.
It is a bit hard to understand what your application is trying to do, but my best guess is that your 'portal' sits between the user's browser and the application, and you are trying to use the some stored credentials for the application to authenticate on behalf of the users.
There are two things you need to watch for / deal with.
The responses from the application will contain SetCookie headers of some sort. The cookies need to be handled carefully. Depending on the security model you are using:
They could be saved in the portal and used for future requests to the application.
They could be relayed to the user's browser. The portal would also need to pass the cookies through in future requests to the application. (This approach needs to be handled carefully to deal with possible issues with session token leakage.)
Also, be aware that SpringSecurity changes the session cookie when login succeeds. If you don't capture the new session cookie and use them in follow on requests to the application, those requests won't be authenticated.
The application's login mechanism is clearly trying to redirect you (the portal) to the "default" place after logging in, and this is inappropriate. There are two simple fixes for this:
Have the portal detect the final redirect and treat it as an indication that you've successfully logged in. Then have the portal repeat the request for the page you were originally requesting from the application using the new cookie (see above).
IIRC, there's an extra parameter you can add to a j_spring_security_check request that tells the application where to return on successful login. I can't recall the details ...
I thought that forwarding the setCookie response header from the RA into the portal's response to the browser would be all that is needed to transfer the cookie/session id to the user's new browser window. Is that not correct?
That will cause the browser to set the RA's cookie for the portal context. That won't work unless the RA and portal are in the cookie's "scope" (for the want of a better word).
Question is, how do I display this on/through the portal? Do I just have to copy all the content over and map all the relative links accordingly? And, as you state, continue to proxy all requests to the app through the portal, passing the cookie each time? Is there any way to avoid copying/modifying the markup?
You do need to massage the markup. But exactly what massaging is required is not entirely clear. I think you'll need to map the relative links so that when the user's browser sees them they point to the portal. Then, arrange that the portal relays requests to the RA with the appropriate cookies.
One tool that you can use to deal with relative links is the HTML <base> element. In fact, this potentially easier to deal with than absolute links ... if you map everything via the portal.
But beware that there are all sorts of things that can cause grief in this process. For example, you've got to beware of the "same source" restriction, and with javascript with embedded URLs for the RA.
In case anyone is interested, here's how everything turned out.
Once we realized the issue with setting foreign cookies, we decided we had a few options:
Proxy - Tunnel through the portal to the
remote application, using the portal
as a proxy. This option is the most
straightforward logically, but it
has complications as mentioned above
(i.e. you have to modify each
request and each response - adding
cookies and markup as necessary).
This method turned out to be a pain
point for us, not unrelated to our
use of IBM WebSphere Portal 7.
3rd party SSO solution - Use CAS or Tivoli or some other enterprise solution. This is our
ideal final solution, but it is
still being researched to determine
compatibility with our environment.
Cookie Monster - Our interim solution, in order to
get IBM portal out of the way as the
middle man, was to deploy a small
new remote application on the same
server as our target app that simply
accepts a cookie in JSON format and
spits it back to the browser in a
302 redirect response.
The cookie monster solution works as follows: when the
user clicks on the link in the
portal, our portlet will internally
lookup the user's credentials,
authenticate to the remote
application, and return the
authentication cookie/token. We
convert that (as well as the
destination URL) to JSON and return
it to the browser. The browser then
posts this JSON to the remote cookie
application in a new window. The
cookie is reconstituted and placed
in the response along with the 302
and the target location. Voila, the
page redirects to the application
homepage and the user is logged in. Yay!
Some notes for anyone using IBM WebSphere Portal:
We handled the authentication via
resource-serving portlet.
Make sure the response from the resource-serving portlet is not cached (we made the cache expire immediately as we could not return no-cache)
Make sure you ping the portal before making the ajax call as the session may be expired.
I'm sure there are other, more elegant solutions, but this is working for us until we get CAS/Tivoli up and running.

Categories

Resources