How do I programmatically call authenticate from within a servlet like j_security_check would do

5.5k views Asked by At

We have the web based form login authentication with j_securtiy_check working. We'd like to change it by programmatic login authentication. What is the proper way of having a servlet authenticate a user name and password passed to it? The servlet is obviously unprotected.

We have been experimenting with this server.xml Realm:

<Realm  className="org.apache.catalina.realm.DataSourceRealm"
    dataSourceName="UserDatabase"
    userTable="app_user" userNameCol="login_name" userCredCol="password_value"
    userRoleTable="user_perm" roleNameCol="permission_name"
    allRolesMode="authOnly" digest="MD5"
/>

The reason for this, is that we have a java webstart client that sends login information to an unprotected loginServlet. This servlet currently authenticates against a JOSSO single sign-on service but I wish to remove this and use simple tomcat7 authentication for starters. Then eventually migrate to OpenAM. If I could programmatically generate the JSSESSIONIDSSO value and stuff this into a cookie.

This is some code that I found. Is this the right way to invoke authentication?

ApplicationContextFacade acf = (ApplicationContextFacade) this.getServletContext();

Field privateField = ApplicationContextFacade.class.getDeclaredField("context");  
privateField.setAccessible(true);  
ApplicationContext appContext = (ApplicationContext) privateField.get(acf);  
Field privateField2 = ApplicationContext.class.getDeclaredField("context");  
privateField2.setAccessible(true);  
StandardContext stdContext = (StandardContext) privateField2.get(appContext);  
Realm realm = stdContext.getRealm();  

Principal principal = realm.authenticate(loginBean.getUsername(), loginBean.getPassword());  
if (principal == null)
{
   return 0;
}
GenericPrincipal genericPrincipal = (GenericPrincipal) principal;

System.out.println ("genericPrincipal=" + genericPrincipal.toString());
4

There are 4 answers

0
D-Klotz On BEST ANSWER

I noticed that this is no longer up to date. The final solution was to use the Java SDK that OpenAM provides.

This is the starting point: http://openam.forgerock.org/openam-documentation/openam-doc-source/doc/dev-guide/index/chap-jdk.html

1) add all of the jar files that come with this SDK to your web application. 2) Change your servlet (or heavy client) to have the following code:

    private void addLoginCallbackMessage(LoginCredentialsBean loginBean, Callback [] callbacks)
        throws UnsupportedCallbackException
{
    int i = 0;
    try
    {
        for (i = 0; i < callbacks.length; i++)
        {
            if (callbacks[i] instanceof TextOutputCallback)
            {
                handleTextOutputCallback((TextOutputCallback) callbacks[i]);
            }
            else if (callbacks[i] instanceof NameCallback)
            {
                handleNameCallback(loginBean.getUsername(), (NameCallback) callbacks[i]);
            }
            else if (callbacks[i] instanceof PasswordCallback)
            {
                handlePasswordCallback(loginBean.getPassword(), (PasswordCallback) callbacks[i]);
            }
            else if (callbacks[i] instanceof TextInputCallback)
            {
                handleTextInputCallback((TextInputCallback) callbacks[i]);
            }
            else if (callbacks[i] instanceof ChoiceCallback)
            {
                handleChoiceCallback((ChoiceCallback) callbacks[i]);
            }
        }
    }
    catch (IOException e)
    {
        e.printStackTrace();
        throw new UnsupportedCallbackException(callbacks[i], e.getMessage());
    }
}

private void handleTextOutputCallback(TextOutputCallback toc)
{
    System.out.println("Got TextOutputCallback");
    // display the message according to the specified type

    switch (toc.getMessageType())
    {
    case TextOutputCallback.INFORMATION:
        System.out.println(toc.getMessage());
        break;
    case TextOutputCallback.ERROR:
        System.out.println("ERROR: " + toc.getMessage());
        break;
    case TextOutputCallback.WARNING:
        System.out.println("WARNING: " + toc.getMessage());
        break;
    default:
        System.out.println("Unsupported message type: " +
                toc.getMessageType());
    }
}

private void handleNameCallback(String name, NameCallback nc)
        throws IOException
{
    nc.setName(name);
}

private void handleTextInputCallback(TextInputCallback tic)
        throws IOException
{
    // not supported for server side
    // prompt for text input
}

private void handlePasswordCallback(String password, PasswordCallback pc)
        throws IOException
{
    // prompt the user for sensitive information

    pc.setPassword(password.toCharArray());
}

private void handleChoiceCallback(ChoiceCallback cc)
        throws IOException
{
    // not supported for server side

    // ignore the provided defaultValue
    /*        
    System.out.print(cc.getPrompt());

    String [] strChoices = cc.getChoices();
    for (int j = 0; j < strChoices.length; j++)
    {
        System.out.print("choice[" + j + "] : " + strChoices[j]);
    }
    System.out.flush();
    cc.setSelectedIndex(Integer.parseInt((new BufferedReader
            (new InputStreamReader(System.in))).readLine()));
    */
}


private void doLogin ()
{
    // ... lots of other logic here

    // TODO: Make this into modules with this one being for OpenAM
    if (_useOpenAM)
    {
        String orgName = "/";
        String moduleName = "DataStore";
        String locale = "en_US";

        AuthContext lc = new AuthContext(orgName);
        AuthContext.IndexType indexType = AuthContext.IndexType.MODULE_INSTANCE;
        lc.login(indexType, moduleName, locale);

        boolean succeed = false;
        Callback [] callbacks = null;

        // get information requested from module
        while (lc.hasMoreRequirements())
        {
            callbacks = lc.getRequirements();
            if (callbacks != null)
            {
                addLoginCallbackMessage(loginBean, callbacks);
                lc.submitRequirements(callbacks);
            }
        }

        if (lc.getStatus() == AuthContext.Status.SUCCESS)
        {
            try
            {
                System.out.println("Login succeeded.");
                openAMSessionId = lc.getAuthIdentifier();
                System.out.println("lc.getAuthIdentifier()=" + openAMSessionId);
                System.out.println("lc.getSuccessURL()=" + lc.getSuccessURL());
                System.out.println("lc.getSSOToken().getAuthLevel()=" + lc.getSSOToken().getAuthLevel());
                System.out.println("lc.getSSOToken().getAuthType()=" + lc.getSSOToken().getAuthType());
                System.out.println("lc.getSSOToken().getHostName()=" + lc.getSSOToken().getHostName());
                System.out.println("lc.getSSOToken().getIdleTime()=" + lc.getSSOToken().getIdleTime());
                System.out.println("lc.getSSOToken().getMaxIdleTime()=" + lc.getSSOToken().getMaxIdleTime());
                System.out.println("lc.getSSOToken().getMaxSessionTime()=" + lc.getSSOToken().getMaxSessionTime());
                System.out.println("lc.getSSOToken().getTimeLeft()=" + lc.getSSOToken().getTimeLeft());
                System.out.println("lc.getSSOToken().getIPAddress()=" + lc.getSSOToken().getIPAddress());
                System.out.println("lc.getSSOToken().getTokenID()=" + lc.getSSOToken().getTokenID().toString());
                System.out.println("lc.getSSOToken().getPrincipal()=" + lc.getSSOToken().getPrincipal().toString());
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }

            succeed = true;
        }
        else if (lc.getStatus() == AuthContext.Status.FAILED)
        {
            System.out.println("Login failed.");
        }
        else
        {
            System.out.println("Unknown status: " + lc.getStatus());
        }

        System.out.println( "OpenAM login success=" + succeed);
    }
}

The important thing from the above code is the variable openAMSessionId . That ends up having the new OpenAM single sign on session id that you can pass around to all of your protected client applications so that the user doesn't get challenged for login.

I hope this helps.

-dklotz

3
LHA On

I think in Java webstart client app, when you need to ask authentication, you just use any HTTP client to sent userName, password to your LoginServer using POST method. In loginServlet, you use request.login ( userName, password ) then return authentication result in any format ( XML, JSON). At client side, you have to parse authentication result ( POST result ) and JSESSIONID cookie from response header too. For subsequent requests, you may have to send JSESSIONID that you parsed before.

0
D-Klotz On

I wanted to follow up on this.

There really isn't a simple answer.

The code at the end uses pure reflection to attempt to call the authenticate method within the realm. The problem is that this really depends on the realm attached.

JOSSO (org.josso.tc55.agent.jaas.CatalinaJAASRealm) for example doesn't have this method. Instead it has something called createPrincipal(String username, Subject subject). Their suggested process for doing this (for at least josso 1.5) is to use code like this:

            impl = getIdentityProvider(endpoint);
            String assertion =   impl.assertIdentityWithSimpleAuthentication(username,password);
            sessionID = impl.resolveAuthenticationAssertion(assertion);

If you use OpenAM (which is what I'm attempting to move to) as your single sign on provider instead of JOSSO, it is completely different. The current idea I am going with is to use a RESTful service they provide directly from the webstart client.

My first issue with that idea - is trying to find an API that I can use from the webstart java client that 1) Doesn't have a huge jar file size, 2) works with tomee+ CXF version 2.6.4. (I don't know enough about this to say, "yes just use the CXF 3.0 client jars as they will work fine with tomee+'s version of CXF...")

Anyhow here is the code that 'should' work if you use Tomcat7's canned datasource mechanisms for setting up a realm.

            Class c = Class.forName("org.apache.catalina.core.ApplicationContextFacade");
            Object o = this.getServletContext();
            System.out.println ("servletContext is really:" + o.getClass().getCanonicalName());

            Field privateField = o.getClass().getDeclaredField("context");  
            privateField.setAccessible(true);  
            Object appContext =  privateField.get(o);  
            Field privateField2 = appContext.getClass().getDeclaredField("context");  
            privateField2.setAccessible(true);  
            Object stdContext =  privateField2.get(appContext);
            Method getRealm = stdContext.getClass().getMethod("getRealm");
            Object realm = getRealm.invoke(stdContext);

            Principal principal = null;
            try
            {
                Method authenticate = realm.getClass().getMethod("authenticate");  
                principal = (Principal)authenticate.invoke(realm, loginBean.getUsername(), loginBean.getPassword());
                if (principal == null)
                {
                    return 0;
                }
            }
            catch (Exception e2)
            {
                // The authenticate method doesn't exist within the configured server.xml realm
                e2.printStackTrace();
            }

Again, this is if you find yourself trying to authenticate a user from within an unprotected servlet.

-Dennis

0
Rustam On

If you're already on Servlet 3.0 or newer, for programmatic authentication use login() method of HttpServletRequest.

if (request.getUserPrincipal() == null) {
    request.getSession(); // create session before logging in
    request.login(username, password);
}

Servlet API provides you login() and logout() methods for programmatic access to container managed security.