In this example, the backend application is a JSF/Spring application that has Spring Security wired in to protect services with both Basic and Form-based authentication. Basic authentication will kick in if a "Authorization" header is sent, otherwise Form-based authentication is used. Here's the Spring Security context file that makes this happen:
01.
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
?>
02.
03.
<
beans:beans
xmlns
=
"http://www.springframework.org/schema/security"
04.
xmlns:beans
=
"http://www.springframework.org/schema/beans"
05.
xmlns:xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
06.
xsi:schemaLocation
=
"..."
>
07.
08.
<
http
auto-config
=
"true"
realm
=
"My Web Application"
>
09.
<
intercept-url
pattern
=
"/faces/welcome.jspx"
access
=
"ROLE_USER"
/>
10.
<
intercept-url
pattern
=
"/*.rpc"
access
=
"ROLE_USER"
/>
11.
<
http-basic
/>
12.
<
form-login
login-page
=
"/faces/login.jspx"
authentication-failure-url
=
"/faces/accessDenied.jspx"
13.
login-processing-url
=
"/j_spring_security_check"
default-target-url
=
"/redirect.jsp"
14.
always-use-default-target
=
"true"
/>
15.
</
http
>
16.
17.
<
authentication-provider
>
18.
<
user-service
>
19.
<
user
name
=
"admin"
password
=
"admin"
authorities
=
"ROLE_USER"
/>
20.
</
user-service
>
21.
</
authentication-provider
>
22.
</
beans:beans
>
The easiest way to configure your GWT application to talk to a Spring Security protected resource is to protect your HTML page that GWT is embedded in. This is the documented way to integrate GWT with Spring Security (ref: GWT's LoginSecurityFAQ, search for "Acegi"). This works well for production, but not for hosted-mode development.
Basic Authentication
To authenticate with Basic Authentication, you can use GWT's RequestBuilder and set an "Authentication" header that contains the user's (base64-encoded) credentials.
01.
private
class
LoginRequest {
02.
public
LoginRequest(RequestCallback callback) {
03.
String url =
"/services/faces/welcome.jspx"
;
04.
05.
RequestBuilder rb =
new
RequestBuilder(RequestBuilder.POST, url);
06.
rb.setHeader(
"Authorization"
, createBasicAuthToken());
07.
rb.setCallback(callback);
08.
try
{
09.
rb.send();
10.
}
catch
(RequestException e) {
11.
Window.alert(e.getMessage());
12.
}
13.
}
14.
}
15.
16.
protected
String createBasicAuthToken() {
17.
byte
[] bytes = stringToBytes(username.getValue() +
":"
+ password.getValue());
18.
String token = Base64.encode(bytes);
19.
return
"Basic "
+ token;
20.
}
21.
22.
protected
byte
[] stringToBytes(String msg) {
23.
int
len = msg.length();
24.
byte
[] bytes =
new
byte
[len];
25.
for
(
int
i =
0
; i < len; i++)
26.
bytes[i] = (
byte
) (msg.charAt(i) &
0xff
);
27.
return
bytes;
28.
}
To use this LoginRequest class, create it with a callback and look for a 401 response code to determine if authentication failed.
01.
new
LoginRequest(
new
RequestCallback() {
02.
public
void
onResponseReceived(Request request, Response response) {
03.
if
(response.getStatusCode() != Response.SC_UNAUTHORIZED &&
04.
response.getStatusCode() != Response.SC_OK) {
05.
onError(request,
new
RequestException(response.getStatusText() +
":"n"
+ response.getText()));
06.
return
;
07.
}
08.
09.
if
(response.getStatusCode() == Response.SC_UNAUTHORIZED) {
10.
Window.alert(
"You have entered an incorrect username or password. Please try again."
);
11.
}
else
{
12.
// authentication worked, show a fancy dashboard screen
13.
}
14.
}
15.
16.
public
void
onError(Request request, Throwable throwable) {
17.
Window.alert(throwable.getMessage());
18.
}
19.
});
If your GWT application is included in the "services" war, everything should work at this point. However, if you try to login with invalid credentials, your browser's login dialog will appear. To suppress this in the aforementioned ProxyServlet, you'll need to make a change in its executeProxyRequest() method so the "WWW-Authenticate" header is not copied.
01.
// Pass the response code back to the client
02.
httpServletResponse.setStatus(intProxyResponseCode);
03.
04.
// Pass response headers back to the client
05.
Header[] headerArrayResponse = httpMethodProxyRequest.getResponseHeaders();
06.
for
(Header header : headerArrayResponse) {
07.
if
(header.getName().equals(
"Transfer-Encoding"
) && header.getValue().equals(
"chunked"
) ||
08.
header.getName().equals(
"Content-Encoding"
) && header.getValue().equals(
"gzip"
) ||
09.
header.getName().equals(
"WWW-Authenticate"
)) {
// don't copy WWW-Authenticate header
10.
}
else
{
11.
httpServletResponse.setHeader(header.getName(), header.getValue());
12.
}
13.
}
I'm not sure how to suppress the browser prompt when not using the ProxyServlet. If you have a solution, please let me know.
Basic Authentication works well for GWT applications because you don't need additional logic to retain the authenticated state after the initial login. While Basic Authentication over SSL might offer a decent solution, the downside is you can't logout. Form-based Authentication allows you to logout.
Form-based AuthenticationBefore I show you how to implement form-based authentication, you should be aware that Google does not recommend this. Below is a warning from their LoginSecurityFAQ.
Do NOT attempt to use the Cookie
header to transfer the sessionID from GWT to the server; it is fraught with security issues that will become clear in the rest of this article. You MUST transfer the sessionID in the payload of the request. For an example of why this can fail, see CrossSiteRequestForgery.
In my experiment, I didn't want to change the server-side Spring Security configuration, so I ignored this warning. If you know how to configure Spring Security so it looks for the sessionID in the payload of the request (rather than in a cookie), I'd love to hear about it. The upside of the example below is it should work with container-managed authentication as well.
The LoginRequest class for form-based authentication is similar to the previous one, except it has a different URL and sends the user's credentials in the request body.
01.
private
class
LoginRequest {
02.
public
LoginRequest(RequestCallback callback) {
03.
String url =
"/services/j_spring_security_check"
;
04.
05.
RequestBuilder rb =
new
RequestBuilder(RequestBuilder.POST, url);
06.
rb.setHeader(
"Content-Type"
,
"application/x-www-form-urlencoded"
);
07.
rb.setRequestData(
"j_username="
+ URL.encode(username.getValue()) +
08.
"&j_password="
+ URL.encode(password.getValue()));
09.
10.
rb.setCallback(callback);
11.
try
{
12.
rb.send();
13.
}
catch
(RequestException e) {
14.
Window.alert(e.getMessage());
15.
}
16.
}
17.
}
If you deploy your GWT application in the same WAR your services are hosted in, this is all you'll need to do. If you're using the ProxyServlet, there's a couple of changes you'll need to make in order to set/send cookies when running in hosted mode.
First of all, you'll need to make sure you've configured the servlet to follow redirects (by subclassing or simply modifying its default). After that, add the following logic on line 358 (or just look for "if (followRedirects)
") to expose the sessionID to the client. The most important part is setting the cookie's path to "/" so the client (running at localhost:8888) can see it.
01.
if
(followRedirects) {
02.
// happens on first login attempt
03.
if
(stringLocation.contains(
"jsessionid"
)) {
04.
Cookie cookie =
new
Cookie(
"JSESSIONID"
,
05.
stringLocation.substring(stringLocation.indexOf(
"jsessionid="
) +
11
));
06.
cookie.setPath(
"/"
);
07.
httpServletResponse.addCookie(cookie);
08.
// the following happens if you refresh your GWT app after already logging in once
09.
}
else
if
(httpMethodProxyRequest.getResponseHeader(
"Set-Cookie"
) !=
null
) {
10.
Header header = httpMethodProxyRequest.getResponseHeader(
"Set-Cookie"
);
11.
String[] cookieDetails = header.getValue().split(
";"
);
12.
String[] nameValue = cookieDetails[
0
].split(
"="
);
13.
14.
Cookie cookie =
new
Cookie(nameValue[
0
], nameValue[
1
]);
15.
cookie.setPath(
"/"
);
16.
httpServletResponse.addCookie(cookie);
17.
}
18.
httpServletResponse.sendRedirect(stringLocation.replace(getProxyHostAndPort() +
19.
this
.getProxyPath(), stringMyHostName));
20.
return
;
21.
}
Click here to see a screenshot of the diff of the ProxyServlet after this code has been added.
Figuring out that headers needed to be parsed after authenticating successfully and before redirecting was the hardest part for me. If you grab the JSESSIONID from the "Set-Cookie" header anywhere else, the JSESSIONID is one that hasn't been authenticated. While the login will work, subsequent calls to services will fail.
To make subsequent calls with the cookie in the header, you'll need to make an additional modification to ProxyServlet to send cookies as headers. First of all, add a setProxyRequestCookies() method:
01.
/**
02.
* Retrieves all of the cookies from the servlet request and sets them on
03.
* the proxy request
04.
*
05.
* @param httpServletRequest The request object representing the client's
06.
* request to the servlet engine
07.
* @param httpMethodProxyRequest The request that we are about to send to
08.
* the proxy host
09.
*/
10.
@SuppressWarnings
(
"unchecked"
)
11.
private
void
setProxyRequestCookies(HttpServletRequest httpServletRequest,
12.
HttpMethod httpMethodProxyRequest) {
13.
// Get an array of all of all the cookies sent by the client
14.
Cookie[] cookies = httpServletRequest.getCookies();
15.
if
(cookies ==
null
) {
16.
return
;
17.
}
18.
19.
for
(Cookie cookie : cookies) {
20.
cookie.setDomain(stringProxyHost);
21.
cookie.setPath(httpServletRequest.getServletPath());
22.
httpMethodProxyRequest.setRequestHeader(
"Cookie"
, cookie.getName() +
23.
"="
+ cookie.getValue() +
"; Path="
+ cookie.getPath());
24.
}
25.
}
Next, in the doGet() and doPost() methods, add the following line just after the call to setProxyRequestHeaders().
1.
setProxyRequestCookies(httpServletRequest, getMethodProxyRequest);
2.
After making these modifications to ProxyServlet, you can create LoginRequest and attempt to authenticate. To detect a failed attempt, I'm looking for text in Spring Security's "authentication-failure-url" page.
01.
new
LoginRequest(
new
RequestCallback() {
02.
03.
public
void
onResponseReceived(Request request, Response response) {
04.
if
(response.getStatusCode() != Response.SC_OK) {
05.
onError(request,
new
RequestException(response.getStatusText() +
":"n"
+ response.getText()));
06.
return
;
07.
}
08.
09.
if
(response.getText().contains(
"Access Denied"
)) {
10.
Window.alert(
"You have entered an incorrect username or password. Please try again."
);
11.
}
else
{
12.
// authentication worked, show a fancy dashboard screen
13.
}
14.
}
15.
16.
public
void
onError(Request request, Throwable throwable) {
17.
Window.alert(throwable.getMessage());
18.
}
19.
});
After making these changes, you should be able to authenticate with Spring Security's form-based configuration. While this example doesn't show how to logout, it should be easy enough to do by 1) deleting the JSESSIONID cookie or 2) calling the Logout URL you have configured in your services WAR.
Hopefully this howto gives you enough information to configure your GWT application to talk to Spring Security without modifying your existing backend application. It's entirely possible that Spring Security offers a more GWT-friendly authentication mechanism. If you know of a better way to integrate GWT with Spring Security, I'd love to hear about it.
Update on October 7, 2009: I did some additional work on this and got Remember Me working when using form-based authentication. I found I didn't need as much fancy logic in my ProxyServlet and was able to reduce the "followRequests" logic to the following:
01.
if
(followRedirects) {
02.
if
(httpMethodProxyRequest.getResponseHeader(
"Set-Cookie"
) !=
null
) {
03.
Header[] headers = httpMethodProxyRequest.getResponseHeaders(
"Set-Cookie"
);
04.
if
(headers.length ==
1
) {
05.
extractCookieFromHeader(httpServletResponse, headers[
0
]);
06.
}
else
{
07.
// ignore the first header since there always seems two jsessionid headers
08.
// and the 2nd is the valid one
09.
for
(
int
i =
1
; i < headers.length; i++) {
10.
extractCookieFromHeader(httpServletResponse, headers[i]);
11.
}
12.
}
13.
}
14.
httpServletResponse.sendRedirect(
15.
stringLocation.replace(getProxyHostAndPort() + getProxyPath(), stringMyHostName));
16.
return
;
17.
}
I was also able to remove the setProxyRequestCookies() method completely as it no longer seems necessary.
Next, I'd like to figure out how to make Spring Security more Ajax-friendly where it can read an authentication token in the request body or header (instead of from a cookie). Also, it'd be sweet if I could convince it to return error codes instead of the login page (for example, when a certain header is present). Posted in Java at Aug 06 2009, 08:50:15 AM MDT 9 Comments
Posted by rahul somasunderam on August 06, 2009 at 10:01 AM MDT #
> I remember reading somewhere that you shouldn't tie a GWT app to the jSessionId.
Maybe you read it in my post above?
I also said: If you know how to configure Spring Security so it looks for the sessionID in the payload of the request (rather than in a cookie), I'd love to hear about it.
;-)
Posted by Matt Raible on August 06, 2009 at 10:10 AM MDT #
Posted by Jason Carreira on August 06, 2009 at 11:40 AM MDT #
Posted by Sakuraba on August 07, 2009 at 02:58 AM MDT #
Howdy,
Which Base64 client-side implementation did you use for your test?
Cheers!
Posted by Lucas on September 07, 2009 at 02:19 PM MDT #
Lucas - below is the class I used for Base64 encoding:
01.
/**
02.
* Base64 MIME content transfer encoding.
03.
*
04.
* @see http://en.wikipedia.org/wiki/Base64
05.
*/
06.
public
class
Base64 {
07.
08.
private
static
final
String ALPHABET =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
;
09.
10.
// Base-64 pad character "="
11.
private
static
final
String PAD =
"="
;
12.
13.
/**
14.
* Convert an array of big-endian words to a base-64 string
15.
*/
16.
public
static
String encode(
int
[] arr,
int
byteLen) {
17.
return
encode(toByteArray(arr, byteLen));
18.
}
19.
20.
private
static
final
int
[] BSHIFT = {
24
,
16
,
8
,
0
};
21.
22.
protected
static
byte
[] toByteArray(
int
[] arr,
int
byteLen) {
23.
byte
[] bytes =
new
byte
[byteLen];
24.
int
l = Math.min(arr.length *
4
, byteLen);
25.
int
i =
0
;
26.
for
(
int
b =
0
; b < l; b++) {
27.
bytes[b] = (
byte
)(arr[i] >> BSHIFT[b &
3
] &
0xff
);
28.
}
29.
return
bytes;
30.
}
31.
32.
public
static
String encode(
byte
[] arr) {
33.
StringBuilder sb =
new
StringBuilder();
34.
int
l = arr.length;
35.
int
m = l %
3
;
36.
l -= m;
37.
for
(
int
i =
0
; i < l; i +=
3
) {
38.
encodeTriplet(sb, arr, i,
3
);
39.
}
40.
if
(m ==
2
) {
41.
encodeTriplet(sb, arr, l,
2
);
42.
}
else
if
(m ==
1
) {
43.
encodeTriplet(sb, arr, l,
1
);
44.
}
45.
return
sb.toString();
46.
}
47.
48.
private
static
void
encodeTriplet(StringBuilder sb,
byte
[] array,
int
index,
int
len) {
49.
int
triplet = (array[index] &
0xFF
) <<
16
;
50.
if
(len >=
2
) triplet |= (array[index +
1
] &
0xFF
) <<
8
;
51.
if
(len >=
3
) triplet |= (array[index +
2
] &
0xFF
);
52.
int
pad =
3
- len;
53.
for
(
int
j =
3
; j >= pad; j--) {
54.
int
p = (triplet >> (j *
6
)) &
0x3F
;
55.
sb.append(ALPHABET.charAt(p));
56.
}
57.
while
(pad-- >
0
) sb.append(PAD);
58.
}
59.
}
Posted by Matt Raible on September 08, 2009 at 08:10 AM MDT #
I tried the above example and i always got a 200 OK status response instead of the suggested 404 @ user is not authenticated, I am not using any proxy servlets...
This is the problem defination in detail:
http://stackoverflow.com/questions/1795474/using-request-builder-to-authenticate-user-not-working-in-spring-security
Posted by salvin francis on November 25, 2009 at 01:58 AM MST #
Posted by Salvin francis on November 25, 2009 at 02:48 AM MST #
Posted by Eric Jablow on December 04, 2009 at 10:22 AM MST #