RagHaven
RagHaven

Reputation: 4347

Web CAS authentication on Android

I am trying to login into a CAS system via Android and I am not sure how to approach this.

This stackoverflow link talks about something similar, but I was not able to understand the solution to the problem. I have no prior experience in authentication protocols and HTTP. I would appreciate any and all help!

EDIT: I was able to find a CAS Client for Android on GitHub, and I tried to use it to see if I could authenticate properly. Unfortunately, I am still having issues. When I execute the login() command I get the following error:

01-20 16:47:19.322: D/CASCLIENT(22682): Ready to get LT from https://www.purdue.edu/apps/account/cas/login?service=http://watcher.rcac.purdue.edu/nagios
01-20 16:47:21.825: D/CASCLIENT(22682): Response = HTTP/1.1 200 OK
01-20 16:47:21.875: D/CASCLIENT(22682): LT=LT-137794-1UkrL1jXJGPMZfuuVDn4RXbcQ3kfCQ
01-20 16:47:21.875: D/CASCLIENT(22682): POST https://www.purdue.edu/apps/account/cas/login?service=http://watcher.rcac.purdue.edu/nagios
01-20 16:47:23.186: D/CASCLIENT(22682): POST RESPONSE STATUS=200 : HTTP/1.1 200 OK
01-20 16:47:23.186: I/CASCLIENT(22682): Authentication to service 'http://watcher.rcac.purdue.edu/nagios' unsuccessul for username .

Here is the CAS Client Code:

public class CasClient
{
    private static final String TAG = "CASCLIENT";
    private static final String CAS_LOGIN_URL_PART = "login";
    private static final String CAS_LOGOUT_URL_PART = "logout";
    private static final String CAS_SERVICE_VALIDATE_URL_PART = "serviceValidate";
    private static final String CAS_TICKET_BEGIN = "ticket=";
    private static final String CAS_LT_BEGIN = "name=\"lt\" value=\"";
    private static final String CAS_USER_BEGIN = "<cas:user>";
    private static final String CAS_USER_END = "</cas:user>";

    /**
     *  An HTTP client (browser replacement) that will interact with the CAS server.
     *  Usually provided by the user, as it is this client that will be "logged in" to
     *  the CAS server. 
     */
    private HttpClient httpClient;
    /**
     * This is the "base url", or the root URL of the CAS server that is will be
     * providing authentication services. If you use <code>http://x.y.z/a/login</code> to login
     * to your CAS, then the base URL is <code>http://x.y.z/a/"</code>. 
     */
    private String casBaseURL;

    /**
     * Construct a new CasClient which uses the specified HttpClient
     * for its HTTP calls. If the CAS authentication is successful, it is the supplied HttpClient to
     * which the acquired credentials are attached.
     *
     * @param   httpClient The HTTP client ("browser replacement") that will 
     *          attempt to "login" to the CAS.
     * @param   casBaseUrl The base URL of the CAS service to be used. If you use 
     *          <code>http://x.y.z/a/login</code> to login to your CAS, then the base URL 
     *          is <code>http://x.y.z/a/"</code>.
     */
    public CasClient (HttpClient httpClient, String casBaseUrl)
    {
        this.httpClient = httpClient;
        this.casBaseURL = casBaseUrl;
    }

    /**
     * Authenticate the specified user credentials and request a service ticket for the
     * specified service. If no service is specified, user credentials are checks but no
     * service ticket is generated (returns null). 
     *
     * @param  serviceUrl The service to login for, yielding a service ticket that can be 
     *         presented to the service for validation. May be null, in which case the 
     *         user credentials are validated, but no service ticket is returned by this method.
     * @param  username
     * @param  password
     * @return A valid service ticket, if the specified service URL is not null and the
     *         (login; password) pair is accepted by the CAS server
     * @throws CasAuthenticationException if the (login; password) pair is not accepted
     *         by the CAS server.
     * @throws CasProtocolException if there is an error communicating with the CAS server
     */
    public String login (String serviceUrl, String username, String password) throws CasAuthenticationException, CasProtocolException
    {
        String serviceTicket = null;
        // The login method simulates the posting of the CAS login form. The login form contains a unique identifier
        // or "LT" that is only valid for 90s. The method getLTFromLoginForm requests the login form from the cAS
        // and extracts the LT that we need.  Note that the LT is _service specific_ : We need to use an identical
        // serviceUrl when retrieving and posting the login form.
        String lt = getLTFromLoginForm (serviceUrl);
        if (lt == null)
        {
            Log.d (TAG, "Cannot retrieve LT from CAS. Aborting authentication for '" + username + "'");
            throw new CasProtocolException ("Cannot retrieve LT from CAS. Aborting authentication for '" + username + "'");
        }
        else
        {
            // Yes, it is necessary to include the serviceUrl as part of the query string. The URL must be
            // identical to that used to get the LT.
            Log.d(TAG,"POST " + casBaseURL + CAS_LOGIN_URL_PART + "?service=" + serviceUrl);
            HttpPost httpPost = new HttpPost (casBaseURL + CAS_LOGIN_URL_PART + "?service=" + serviceUrl);
            try
            {
                // Add form parameters to request body
                List <NameValuePair> nvps = new ArrayList <NameValuePair> ();
                nvps.add(new BasicNameValuePair ("_eventId", "submit"));
                nvps.add(new BasicNameValuePair ("username", username));
                nvps.add(new BasicNameValuePair ("gateway", "true"));
                nvps.add(new BasicNameValuePair ("password", password));
                nvps.add(new BasicNameValuePair ("lt", lt));
                httpPost.setEntity(new UrlEncodedFormEntity(nvps));

                // execute post method      
                HttpResponse response = httpClient.execute(httpPost);
                Log.d (TAG, "POST RESPONSE STATUS=" + response.getStatusLine().getStatusCode() + " : " + response.getStatusLine().toString());

                //TODO It would seem that when the client is already authenticated, the CAS server 
                // redirects transparently to the service URL!
                // Success if CAS replies with a 302 HTTP status code and a Location header
                // We assume that if a valid ticket is provided in the Location header, that it is also a 302 HTTP STATUS
                Header headers[] = response.getHeaders("Location");
                if (headers != null && headers.length > 0) 
                    serviceTicket = extractServiceTicket (headers[0].getValue());
                HttpEntity entity = response.getEntity();
                entity.consumeContent();

                if (serviceTicket == null)
                {
                    Log.i (TAG, "Authentication to service '" + serviceUrl + "' unsuccessul for username '" + username + "'.");
                    throw new CasAuthenticationException ("Authentication to service '" + serviceUrl + "' unsuccessul for username '" + username + "'.");
                }
                else
                    Log.i (TAG, "Authentication to service '" + serviceUrl + "' successul for username '" + username + "'.");
            } 
            catch (IOException e) 
            {
                Log.d (TAG, "IOException trying to login : " + e.getMessage());
                throw new CasProtocolException ("IOException trying to login : " + e.getMessage());
            } 
            return serviceTicket;
        }
    }

    /**
     * Logout from the CAS. This destroys all local authentication cookies
     * and any tickets stored on the server.
     * 
     * @return <code>true</false> if the logout is acknowledged by the CAS server
     */
    public boolean logout ()
    {
        boolean logoutSuccess = false;
        HttpGet httpGet = new HttpGet (casBaseURL + CAS_LOGOUT_URL_PART);
        try
        {
            HttpResponse response = httpClient.execute(httpGet);
            logoutSuccess = (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK);
            Log.d (TAG, response.getStatusLine().toString());
        } 
        catch (Exception e)
        {
            Log.d(TAG, "Exception trying to logout : " + e.getMessage());
            logoutSuccess = false;
        } 
        return logoutSuccess;
    }

    /**
     * Validate the specified service ticket against the specified service.
     * If the ticket is valid, this will yield the clear text user name
     * of the authenticated user.
     * 
     * Note that each service ticket issued by CAS can be used exactly once
     * to validate.
     *
     * @param serviceUrl The serviceUrl to validate against
     * @param serviceTicket The service ticket (previously provided by the CAS) for the serviceUrl
     * @return Clear text username of the authenticated user.
     * @throws CasProtocolException if a protocol or communication error occurs
     * @throws CasClientValidationException if the CAS server refuses the ticket for the service
     */
    public String validate (String serviceUrl, String serviceTicket) throws CasAuthenticationException, CasProtocolException
    {
        HttpPost httpPost = new HttpPost (casBaseURL + CAS_SERVICE_VALIDATE_URL_PART );
        Log.d(TAG, "VALIDATE : " + httpPost.getRequestLine());
        String username = null;
        try
        {
            List <NameValuePair> nvps = new ArrayList <NameValuePair> ();
            nvps.add(new BasicNameValuePair ("service", serviceUrl));
            nvps.add(new BasicNameValuePair ("ticket", serviceTicket));
            httpPost.setEntity (new UrlEncodedFormEntity(nvps));
            HttpResponse response = httpClient.execute (httpPost);
            Log.d (TAG, "VALIDATE RESPONSE : " + response.getStatusLine().toString());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode != HttpStatus.SC_OK)
            {
                Log.d (TAG,"Could not validate: " + response.getStatusLine());
                throw new CasAuthenticationException("Could not validate service: " + response.getStatusLine());
            } 
            else
            {   
                HttpEntity entity = response.getEntity();
                username = extractUser (entity.getContent());
                Log.d (TAG, "VALIDATE OK YOU ARE : " + username);
                entity.consumeContent();
            }
        } 
        catch (Exception e)
        {
            Log.d (TAG, "Could not validate: " + e.getMessage ());
            throw new CasProtocolException ("Could not validate : " + e.getMessage ());
        }
        return username;
    }

    /**
     * This method requests the original login form from CAS.
     * This form contains an LT, an initial token that must be
     * presented to CAS upon sending it an authentication request
     * with credentials.
     * 
     * If the (optional) service URL is provided, this method
     * will construct the URL such that CAS will correctly authenticate 
     * against the specified service when a subsequent authentication request 
     * is sent (with the login method).
     *
     * @param serviceUrl
     * @return The LT token if it could be extracted from the CAS response, else null.
     */
    protected String getLTFromLoginForm (String serviceUrl)
    {
        HttpGet httpGet = new HttpGet (casBaseURL + CAS_LOGIN_URL_PART + "?service=" + serviceUrl);

        String lt = null;
        try
        {
            Log.d (TAG, "Ready to get LT from " + casBaseURL + CAS_LOGIN_URL_PART + "?service=" + serviceUrl);
            HttpResponse response = httpClient.execute (httpGet);
            Log.d (TAG, "Response = " + response.getStatusLine().toString());
            if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK)
            {
                Log.d(TAG,"Could not obtain LT token from CAS: " + response.getStatusLine().getStatusCode() + " / " + response.getStatusLine());
            } 
            else
            {
                HttpEntity entity = response.getEntity();
                if (entity != null) lt = extractLt (entity.getContent());
                entity.consumeContent();
                Log.d (TAG, "LT=" + lt);
            }
        } 
        catch (ClientProtocolException e)
        {
            Log.d(TAG, "Getting LT client protocol exception", e);
        } 
        catch (IOException e) 
        {
            Log.d(TAG, "Getting LT io exception",e);
        }

        return lt;
    }

    /**
     * Helper method to extract the user name from a "service validate" call to CAS.
     *
     * @param data Response data.
     * @return The clear text username, if it could be extracted, null otherwise.
     */
    protected String extractUser (InputStream dataStream)
    {
        BufferedReader reader = new BufferedReader (new InputStreamReader(dataStream));
        String user = null;
        try 
        {
            String line = reader.readLine();
            while (user == null && line != null)
            {
                int start = line.indexOf (CAS_USER_BEGIN);
                if (start >= 0)
                {
                    start += CAS_USER_BEGIN.length();
                    int end = line.indexOf(CAS_USER_END, start);
                    user = line.substring (start, end);
                }
                line = reader.readLine();
            }
        } 
        catch (IOException e) 
        {
            Log.d (TAG, e.getLocalizedMessage());
        }
        return user;
    }

    /**
     * Helper method to extract the service ticket from a login call to CAS.
     *
     * @param data Response data.
     * @return The service ticket, if it could be extracted, null otherwise.
     */
    protected String extractServiceTicket (String data)
    {   
        Log.i(TAG, "ST DATA: " +data);
        String serviceTicket = null;
        int start = data.indexOf(CAS_TICKET_BEGIN);
        if (start > 0)
        {
            start += CAS_TICKET_BEGIN.length ();
            serviceTicket = data.substring (start);
        }
        return serviceTicket;
    }


    /**
     * Helper method to extract the LT from the login form received from CAS.
     *
     * @param data InputStream with HTTP response body.
     * @return The LT, if it could be extracted, null otherwise.
     */
    protected String extractLt (InputStream dataStream)
    {
        BufferedReader reader = new BufferedReader (new InputStreamReader(dataStream));
        String token = null;
        try 
        {
            String line = reader.readLine();
            while (token == null && line != null)
            {
                int start = line.indexOf (CAS_LT_BEGIN);
                if (start >= 0)
                {
                    start += CAS_LT_BEGIN.length();
                    int end = line.indexOf("\"", start);
                    token = line.substring (start, end);
                }
                line = reader.readLine();
            }
        } 
        catch (IOException e) 
        {
            Log.d (TAG, e.getMessage());
        }
        return token;
    }

}

Here is my Activity from which I am invoking the CAS Client.

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        HttpClient client = new DefaultHttpClient();


        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();

        StrictMode.setThreadPolicy(policy); 
        CasClient c = new CasClient(client,"https://www.purdue.edu/apps/account/cas/");
        try {
            c.login("http://watcher.rcac.purdue.edu/nagios", "0025215948", "scholar1234");
        } catch (CasAuthenticationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (CasProtocolException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

}

Upvotes: 3

Views: 3224

Answers (2)

moobyfr
moobyfr

Reputation: 71

I got the same problem. I think that the code found on github was designed for an old version of the sdk

basically, the problem reside in testing if the user is really logged in: the CAS server makes a response with a 302, which contain, the location of the service. But the code

httpClient.execute(httpPost) 

follows the redirection and the status of the service acceded with its 200 response. Here is no more location , and the code thinks that login failed...

Edit: I've found the way to get the code running:

Use a replacement jar instead of the bundled (and old) org.apache.http: http://code.google.com/p/httpclientandroidlib/ , the version 4.3 at least.

they ship the latest stable version of the org.apache.http available in a jar format. Next you have to replace all import from org.apache.http by ch.boye.httpclientandroidlib

keep using the example provided by the original author, but, create the httpClient with different option:

HttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new DefaultRedirectStrategy()).build();

This has worked for me.

Upvotes: 2

nKn
nKn

Reputation: 13761

The idea of CAS authentication is not difficult itself, but implementing it having no experience in HTTP may complicate it. It's based on tickets, so when you want to authenticate within a website, you're redirected to the login portal of the CAS site, you have to enter your credentials and they're validated. Surely if they don't match you'll get an error, otherwise a TGT (Ticket Granting Ticket) is generated and returned to your client. So you have to get this ticket and pass it to the CAS authentication servlet every time you do an operation that requires being authenticated. The ticket may expire and in this case the CAS server will send you a new ticket, which must overwrite the last one and this one's the one you need to present.

In this link you have a detailed explaination on how CAS works (basically the workflow), and here you have a Java example and part of the implementation.

Upvotes: 2

Related Questions