čtvrtek 11. června 2009

Web Service with Oracle SSO

Today, I was facing a new challenge: using Axis2 Web Service client, we need to access a User Web Service that uses Single sign-on (SSO) authentication. With a little help of google I finally found the solution. My first problem was SSL. The Oracle SSO service is (of course) using HTTPS protocol and the server's certificate was expired. An easy fix was to download the certificate (using my Mozilla Firefox and export it), create a new truststore and use it in my test case: Create a new truststore with the server's certificate imported:
keytool -import -alias sso.cez -file sso.cez.cert -keystore cez.truststore
Specify the truststore for JSSE and its password in JVM options:
-Djavax.net.ssl.trustStore=cez.truststore
-Djavax.net.ssl.trustStorePassword=changeit
My second problem was the Axis2 Web Service client and the Oracle SSO service. Even if I supplied correct credentials to Axis2 Service client stub for the SSO service, I got the following exception:
11.6.2009 14:21:27 org.apache.commons.httpclient.HttpMethodDirector executeWithRetry
INFO: I/O exception (org.apache.commons.httpclient.NoHttpResponseException) caught when processing request: The server portaltest.fg.cz failed to respond
11.6.2009 14:21:27 org.apache.commons.httpclient.HttpMethodDirector executeWithRetry
INFO: Retrying request
11.6.2009 14:21:27 org.apache.axis2.transport.http.HTTPSender sendViaPost
INFO: Unable to sendViaPost to url[http://portaltest.fg.cz:8080/cezdochtest/services/UserService]
org.apache.commons.httpclient.NoHttpResponseException: The server portaltest.fg.cz failed to respond
 at org.apache.commons.httpclient.HttpMethodBase.readStatusLine(HttpMethodBase.java:1976)
 at org.apache.commons.httpclient.HttpMethodBase.readResponse(HttpMethodBase.java:1735)
 at org.apache.commons.httpclient.HttpMethodBase.execute(HttpMethodBase.java:1098)
 at org.apache.commons.httpclient.HttpMethodDirector.executeWithRetry(HttpMethodDirector.java:398)
 at org.apache.commons.httpclient.HttpMethodDirector.executeMethod(HttpMethodDirector.java:171)
 at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:397)
 at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:346)
 at org.apache.axis2.transport.http.AbstractHTTPSender.executeMethod(AbstractHTTPSender.java:542)
 at org.apache.axis2.transport.http.HTTPSender.sendViaPost(HTTPSender.java:189)
 at org.apache.axis2.transport.http.HTTPSender.send(HTTPSender.java:75)
 at org.apache.axis2.transport.http.CommonsHTTPTransportSender.writeMessageWithCommons(CommonsHTTPTransportSender.java:371)
 at org.apache.axis2.transport.http.CommonsHTTPTransportSender.invoke(CommonsHTTPTransportSender.java:209)
 at org.apache.axis2.engine.AxisEngine.send(AxisEngine.java:448)
 at org.apache.axis2.description.OutInAxisOperationClient.send(OutInAxisOperation.java:401)
 at org.apache.axis2.description.OutInAxisOperationClient.executeImpl(OutInAxisOperation.java:228)
 at org.apache.axis2.client.OperationClient.execute(OperationClient.java:163)
 at cz.elanor.egdoch.webservices.userservice.client.UserServiceStub.jds_prit_osoba(UserServiceStub.java:178)
 at cz.elanor.egdoch.webservices.userservice.client.UserService.getAttendanceStatus(UserService.java:48)
 at cz.elanor.egdoch.webservices.userservice.client.UserServiceTest.testIt(UserServiceTest.java:28)
After a while (looking at the stacktrace and using some HTTP monitors) I figured out that the Oracle SSO service (or something in the way) closes TCP connection forcibly when it detects a POST request with no SSO token header. So I tried to create an easy test to prove my thoughts. Using the HttpClient from apache with GET method and correct credentials I could finally get through to the Web Service:
public void testGet() {
    HttpClient client = new HttpClient();
    HttpMethod method = new GetMethod("http://portaltest.fg.cz:8080/cezdochtest/services/UserService");
    final AuthScope authScope = new AuthScope(AuthScope.ANY);
    client.getState().setCredentials(authScope, new UsernamePasswordCredentials("username", "password"));
    client.getParams().setParameter("http.protocol.allow-circular-redirects", Boolean.TRUE);
    try {
        int result = client.executeMethod(method);
        System.out.println(result);
        System.out.println(new String(method.getResponseBody()));
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        method.releaseConnection();
    }
}
The HTTP client communicates with the Web Service as follows:
  • It issues a GET request, the SSO service detects no SSO token and redirects to Oracle SSO service (HTTP status 302).
  • The client starts HTTPS communication (trusting the server's certificate) and forwards to the SSO, the server responds 401 Unauthorized with BASIC challenge (this challenge can be skiped when client uses preemptive authentication in the previous bullet).
  • The client sends configured credentials, SSO server authenticates the client request and redirects (using HTTP status 302 again – that's why "http.protocol.allow-circular-redirects" must be allowed). To be honest, there is another redirect as part of the Oracel SSO authentication challenge, but it is not so important to our use case.
  • Now, the client has an SSO token in its cookies (authenticated token) and is able to pass through the SSO service to the web service.
Super duper! So, the only thing that we need to do is to obtain the Oracle SSO token for the Axis2 Web Service client - but how? An easy answer is to do a separate HTTP GET request, obtain a token and use it in the Axis2 client's stub. Here comes the final code for my Axis2 Web Service client:
public class UserService {
    private static final Log log = LogFactory.getLog(UserService.class);

    // the service stub
    private UserServiceStub stub;
    private String username;
    private String password;

    public UserService(String endpointAddress) {
        try {
            stub = new UserServiceStub(endpointAddress);
            // reuse the client (due to SSO)
            stub._getServiceClient().getOptions().setProperty(HTTPConstants.REUSE_HTTP_CLIENT, "true");
        } catch (Exception e) {
            log.error("Unable to instantiate a user service due to the following error!", e);
        }
    }

    public UserServiceStub.PritOsobaWrapper getAttendanceStatus(String kpjm, String osc) {
        UserServiceStub.Jds_prit_osobaE osobaE = new UserServiceStub.Jds_prit_osobaE();
        UserServiceStub.Jds_prit_osoba param = new UserServiceStub.Jds_prit_osoba();
        param.setArg0(kpjm);
        param.setArg1(osc);
        osobaE.setJds_prit_osoba(param);
        for (; ;) {
            try {
                final UserServiceStub.Jds_prit_osobaResponseE response = stub.jds_prit_osoba(osobaE);
                return response.getJds_prit_osobaResponse().get_return();
            } catch (RemoteException e) {
                // try to re-authenticate (SSO token might have been expired or missing)
                try {
                    authenticate();
                } catch (AuthenticationException ae) {
                    log.error(e.getMessage(), e);
                    log.error("Unable to authenticate, the HTTP response status is: " + ae.getHttpStatus(), ae);
                    return null;
                }
            }
        }
    }

    protected void authenticate() throws AuthenticationException {
        // a TCP connection is closed (probably by SSO) when a POST with no SSO token is issued. That's why we use
        // an HTTP client with user name credentials supplited to issue a GET request. This request is accepted by the
        // SSO server and it issues an authentication challenge that is handled by the HTTP client
        HttpClient client = new HttpClient();
        HttpMethod method = new GetMethod(stub._getServiceClient().getOptions().getTo().getAddress());
        try {
            final AuthScope authScope = new AuthScope(AuthScope.ANY);
            client.getState().setCredentials(authScope, new UsernamePasswordCredentials(username, password));
            client.getParams().setParameter("http.protocol.allow-circular-redirects", Boolean.TRUE);
            int status = client.executeMethod(method);
            if (status < 200 || status >= 300) {
                throw new AuthenticationException("Unable to authenticate!", status);
            }
            List headers = new ArrayList();
            headers.add(method.getRequestHeader("cookie"));
            stub._getServiceClient().getOptions().setProperty(HTTPConstants.HTTP_HEADERS, headers);
        } catch (IOException e) {
            throw new AuthenticationException(e, -1);
        } finally {
            method.releaseConnection();
        }
    }

    public void setUsernameAndPassword(String username, String password) {
        if (username == null || password == null) {
            log.info("Unable to set username and password for \"osoba\" service, either or both are not specified.");
            return;
        }
        this.username = username;
        this.password = password;
        // try to authenticate
        try {
            authenticate();
        } catch (AuthenticationException ae) {
            log.error("Unable to authenticate, the HTTP response status is: " + ae.getHttpStatus(), ae);
        }
    }

    // use the authentication exception only for clarity
    private class AuthenticationException extends RuntimeException {
        private final int httpStatus;

        private AuthenticationException(String message, int status) {
            super(message);
            this.httpStatus = status;
        }

        private AuthenticationException(Throwable cause, int status) {
            super(cause);
            this.httpStatus = status;
        }

        public int getHttpStatus() {
            return httpStatus;
        }
    }

}
Hope this post is clear (or at least) helpful ;)

Žádné komentáře:

Okomentovat