Friday, June 6, 2014

Pin It


Get Gadget

Twitter Authenticator for WSO2 Identity Server 5.0.0

If you have already used http://wso2.com/products/identity-server/ you knows that it allows us to use custom authenticators. Also it comes with several authenticators which are built in. For a example Facebook, Google, OpenID, SAML are such authenticators. Here in my application there was requirement to authenticate users via Twitter. Thus I used the capability of WSO2 Identity Server which is configured to provide SAML login to my application. In the SAML SSO scenario my users can choose Twitter as there authentication option. To do that I had to write my own authenticator.


To understand the authtication logic form Twitter API side you need to look at,

https://dev.twitter.com/docs/browser-sign-flow
https://dev.twitter.com/docs/auth/implementing-sign-twitter

Also to do these in Java there is a solid library called twitter4j. You need to look at Code Examples of Sign in with Twitter.

At the point which I did this, there were no documentation provided to do this.Using the knowledge I gathered in my internship @ WSO2 and after getting some ideas from experts, I was able to write my authenticator. I had looked at the WSO2 Identity Server code base to see how other authenticators are written.

I will start with the structure of a authenticator pom.xml. Authenticators are OSGi bundles. So the pom.xml looks like this and you can find the dependencies for the project. Other than the twitter4j dependency other dependencies are mandatory.
<?xml version="1.0" encoding="utf-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <groupId>org.emojot</groupId>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>authenticator-twitter</artifactId>
    <packaging>bundle</packaging>
    <version>1.0.0</version>

    <dependencies>

        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.logging</artifactId>
            <version>4.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.identity.application.authentication.framework</artifactId>
            <version>4.2.2</version>
        </dependency>

        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.ui</artifactId>
            <version>4.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.amber.wso2</groupId>
            <artifactId>amber</artifactId>
            <version>0.22.1358727.wso2v4</version>
        </dependency>

        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.identity.application.common</artifactId>
            <version>4.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.twitter4j</groupId>
            <artifactId>twitter4j-core</artifactId>
            <version>[4.0,)</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>wso2-nexus</id>
            <name>WSO2 Internal Repository</name>
            <url>http://maven.wso2.org/nexus/content/groups/wso2-public/</url>
            <releases>
                <enabled>true</enabled>
                <updatePolicy>daily</updatePolicy>
                <checksumPolicy>ignore</checksumPolicy>
            </releases>
        </repository>
        <repository>
            <id>twitter4j.org</id>
            <name>twitter4j.org Repository</name>
            <url>http://twitter4j.org/maven2</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-scr-plugin</artifactId>
                <version>1.7.4</version>
                <executions>
                    <execution>
                        <id>generate-scr-scrdescriptor</id>
                        <goals>
                            <goal>scr</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <extensions>true</extensions>
                <configuration>
                    <instructions>
                        <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
                        <Bundle-Name>${project.artifactId}</Bundle-Name>
                        <Private-Package>org.emojot.authenticator.twitter.internal</Private-Package>
                        <Import-Package>org.twitter4j.*;
                            version="[4.0,)",
                            org.apache.axis2.*;
                            version="[1.6.1.wso2v1, 1.7.0)",
                            org.apache.axiom.*;
                            version="[1.2.11.wso2v2, 1.3.0)",
                            org.wso2.carbon.ui.*,
                            org.apache.commons.logging.*; version="1.0.4",
                            org.osgi.framework,
                            org.wso2.carbon.identity.application.authentication.framework.*,
                            javax.servlet;version="[2.6.0,3.0.0)",
                            javax.servlet.http;version="[2.6.0,3.0.0)",
                            *;resolution:=optional
                        </Import-Package>
                        <Export-Package>!org.emojot.authenticator.twitter.internal,
                            org.emojot.authenticator.twitter.*
                        </Export-Package>
                        <DynamicImport-Package>*</DynamicImport-Package>
                    </instructions>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
Since your project is a OSGi bundle you need add this class to define bundle activate method and deactivate method.
package org.emojot.authenticator.twitter.internal;

import java.util.Hashtable;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.service.component.ComponentContext;
import org.wso2.carbon.identity.application.authentication.framework.ApplicationAuthenticator;
import org.emojot.authenticator.twitter.TwitterAuthenticator;

/**
 * @scr.component name="authenticator.twitter" immediate="true"
 */

public class TwitterAuthenticatorServiceComponent {

    private static final Log LOGGER = LogFactory.getLog(TwitterAuthenticatorServiceComponent.class);

    protected void activate(ComponentContext ctxt) {
        try {
            TwitterAuthenticator twitterAuthenticator = new TwitterAuthenticator();
            Hashtable<String, String> props = new Hashtable<String, String>()
            ctxt.getBundleContext().registerService(ApplicationAuthenticator.class.getName(),twitterAuthenticator, props); 

            LOGGER.info("----Twitter Authenticator bundle is activated----");

        } catch (Throwable e) {
            LOGGER.fatal("----Error while activating Twitter authenticator----", e);
        }
    }

    protected void deactivate(ComponentContext ctxt) {
        LOGGER.info("----Twitter Authenticator bundle is deactivated----");
    }
}
After adding these to you project you are in a position to write your authenticator. Authenticators are defined by extending AbstractApplicationAuthenticator class and implementing    FederatedApplicationAuthenticator interface. The important methods in these two are,
  • public String getName()
  • public String getFriendlyName()
  • public String getContextIdentifier(HttpServletRequest request) - Returns a uniquer identifier which will map the authentication request and the response. The value return by the invocation of authentication request and the response should be the same.
  • public boolean canHandle(HttpServletRequest request) - Tells whether this authenticator can handle the authentication response.
  • protected void initiateAuthenticationRequest(HttpServletRequest request,HttpServletResponse response, AuthenticationContext context) 
  • protected void processAuthenticationResponse(HttpServletRequest request,HttpServletResponse response, AuthenticationContext context)
I have implemented the canHandle() method like this. When Twitter sends the OAuth response it will sends parameters oauth_token,oauth_verifier in the request. Than is a notification to identify that this response can be handled by the authenticator.

    public boolean canHandle(HttpServletRequest request) {
        if (request.getParameter("oauth_token")!=null && request.getParameter("oauth_verifier")!=null) {
            return true;
        }
        return false;
    }

For each authentication request which comes to IS, there is unique value comes as a parameter. That is sessionDataKey. I stored that in the Twitter authentication redirection session to facilitate the requirement of  getContextIdentifier gives same value for authentication request and its response.

    public String getContextIdentifier(HttpServletRequest request) {
        if(request.getSession().getAttribute("contextIdentifier")==null){ 
            request.getSession().setAttribute("contextIdentifier",request.getParameter("sessionDataKey"));
            return request.getParameter("sessionDataKey");
        }else{
            return (String) request.getSession().getAttribute("contextIdentifier");
        }
    } 

I have implemented the initiateAuthenticationRequest method and processAuthenticationResponse method as follows,
     protected void initiateAuthenticationRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException {       

        String apiKey= resourceBundle.getString("API_Key");
        String apiSecret= resourceBundle.getString("API_Secret");

        Twitter twitter = new TwitterFactory().getInstance();
        twitter.setOAuthConsumer(apiKey, apiSecret);
       
        try {
            String callbackURL = resourceBundle.getString("Call_Back_URL");
            RequestToken requestToken = twitter.getOAuthRequestToken(callbackURL.toString());
            request.getSession().setAttribute("requestToken",requestToken);
            request.getSession().setAttribute("twitter",twitter);
            response.sendRedirect(requestToken.getAuthenticationURL());

        } catch (TwitterException e) {
            LOGGER.error("Exception while sending to the Twitter login page.", e);
            throw new AuthenticationFailedException(e.getMessage(), e);
        } catch (IOException e) {
            LOGGER.error("Exception while sending to the Twitter login page.", e);
            throw new AuthenticationFailedException(e.getMessage(), e);
        }
        return;
    }

    protected void processAuthenticationResponse(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException {
        Twitter twitter = (Twitter) request.getSession().getAttribute("twitter");
        RequestToken requestToken = (RequestToken) request.getSession().getAttribute("requestToken");
        String verifier = request.getParameter("oauth_verifier");
        try {
            AccessToken token=twitter.getOAuthAccessToken(requestToken, verifier);
            request.getSession().removeAttribute("requestToken");
            User user= twitter.verifyCredentials();
            buildClaims(user,context);
        } catch (TwitterException e) {
            LOGGER.error("Exception while obtaining OAuth token form Twitter", e);
            throw new AuthenticationFailedException("Exception while obtaining OAuth token form Twitter",e);
        }
    }

    public void buildClaims(User user, AuthenticationContext context) {

            context.setSubject(String.valueOf(user.getId()));
            Map<ClaimMapping, String> claims = new HashMap<ClaimMapping, String>();
            claims.put(ClaimMapping.build("name", "name", null,false), user.getName());
            claims.put(ClaimMapping.build("screen_name", "screen_name", null,false), user.getScreenName());
            claims.put(ClaimMapping.build("url", "url", null,false), user.getURL());

            context.setSubjectAttributes(claims);
    }
The buildClaims method save the retrieved user attributes to the authenticated context in IS. That is need to map the claims to built in claims of IS.

After implementing these methods you can build your bundle. After building it you have to put that in to IS_Home/repository/components/dropins folder. After restarting this you can use the Twitter authenticator in IS.

1 comment:

  1. Great write up. Would you be able to post the full maven project?

    ReplyDelete