9.4. OAuth providers integration

Warning

You are looking at documentation for an older release. Not what you want? See the current release documentation.

About OAuth and OAuth providers

The OAuth 2.0 specification, RFC-6749 by IETF, is an open standard for authorization. It allows resource owners to authorize third-party access to their server resources without sharing their credentials.

To explain this definition, let's say a GitHub user wants to allow a third-party application to access their GitHub data like the profile and repositories. In a traditional way, the user will give the application his username/password. This approach has some defects. For instance, there is no way to identify the application, thus impossible to limit the access. If the user wants to stop using an application, he must change the password, so other applications will be disallowed as well.

In OAuth approach, the third-party application must be registered to (so identified by) GitHub. When the app requests the resources for the first time, GitHub prompts the user so he can allow or refuse the access. On the allowance, the app receives an access token - not the user credentials, and uses it to proceed to access the resource.

By this way, the user can revoke the access anytime. Depending on the OAuth implementation, it can give a policy based on that the user can choose which resources are accessible.

Note

This example just explains OAuth quickly. Please read RFC-6749 to understand the roles and the flow before you continue.

You can find a list of OAuth providers in http://oauth.net/2/

OAuth integration with eXo

The term "OAuth integration" suggests that you can write applications that run at eXo, and access resources from another resource server by getting authorized by an OAuth provider. To avoid a wide topic of data propagation that it may imply, this tutorial only focuses on how to allow users to sign up/log into eXo using other Social network accounts.

As of 4.3, eXo supports login by Facebook, Google+, LinkedIn and Twitter. These four networks are built-in supported.

Behind the scene, there is a framework that resolves the following challenges:

The great point is, the framework allows extending the provider list. In this tutorial you write an addon that allows GitHub users to sign in.

GitHub as an OAuth provider

The GitHub OAuth flow and other information you need can be found at https://developer.github.com/v3/oauth/. You should read it before you continue.

At the moment, it is not clear which scopes are supported. However, the addon will need only the (default) user scope, so it does not matter.

Tip

It is good to know that you can test the flow completely before writing any code. The tip is to use a browser plugin, such as Chrome Advanced Rest client.

For that test, when registering your app, you should set the callback URL to your localhost. You can change it anytime later. In order to see all things work perfectly, you can set up an Apache/Nginx server to return a default page at port 80.

When you start coding, this tutorial requires you to run an eXo instance at localhost:8080 for simplification. So at that time, change the callback URL to this address.

Writing an GitHub addon

So now you have a GitHub application and you tested the flow. From the test you learned the following aspects that will be variables in your code:

VariableDescription
authentication endpoint

https://github.com/login/oauth/authorize

access token endpoint

https://github.com/login/oauth/access_token

profile endpoint

https://api.github.com/user

redirect URL (or callback URL)

In your test the actual value should be http://localhost:8080/portal/githubAuth.

In production it depends on the eXo base URL, so it should be a configuration.

client_id

Should be configurable.

client_secret

Should be configurable.

To be packaged as an eXo Addon, the project should have three modules:

For Java code, the framework requires you to write:

Let's start your coding.

  1. Write a class called GithubAccessTokenContext that wraps the access token in a context. The idea is to provide a getAccessToken() method, but it might also be able to handle the custom scopes in the future, so you should extend the abstract class org.gatein.security.oauth.spi.AccessTokenContext:

    public class GithubAccessTokenContext extends AccessTokenContext implements Serializable {
    
      
      private static final long serialVersionUID = 42L;
      
      private final String accessToken;
      
      public GithubAccessTokenContext(String accessToken) {
        if (accessToken == null) {
          throw new IllegalArgumentException("accessToken must not be null!");
        }
        this.accessToken = accessToken;
      }
      
      @Override
      public String getAccessToken() {
        return accessToken;
      }
      
      @Override
      public boolean equals(Object that) {
        if (!(super.equals(that))) {
          return false;
        }
        GithubAccessTokenContext that_ = (GithubAccessTokenContext)that;
        return this.accessToken.equals(that_.getAccessToken()) ;
      }
      
      public int hashCode() {
        return super.hashCode() * 13 + accessToken.hashCode() * 11;
      }
    }
  2. Write an interface, called GithubProcessor for the major service - that should indeed implements the general org.gatein.security.oauth.spi.OAuthProviderProcessor interface. You need this extended interface for two reasons: 1. to satisfy the key-type pattern of eXo services, and 2. to add any methods you want for GitHub in particular.

    Now there are no extended methods, so it is simple:

    public interface GithubProcessor extends OAuthProviderProcessor<GithubAccessTokenContext> {
    
    
    }
  3. Write the implementation GithubProcessorImpl.

    public class GithubProcessorImpl implements GithubProcessor {
    
    
    }

    It keeps all the information about the provider and the app:

      public static final String AUTHENTICATION_ENDPOINT_URL = "https://github.com/login/oauth/authorize";
    
      public static final String ACCESS_TOKEN_ENDPOINT_URL = "https://github.com/login/oauth/access_token";
      public static final String PROFILE_ENDPOINT_URL = "https://api.github.com/user";
      private final String     redirectURL;
      private final String     clientID;
      private final String     clientSecret;
      private final int        chunkLength;
      private final SecureRandomService secureRandomService;
      public GithubProcessorImpl(ExoContainerContext context, InitParams params, SecureRandomService secureRandomService) {
        String redirectURL_ = params.getValueParam("redirectURL").getValue();
        redirectURL_ = redirectURL_.replaceAll("@@portal.container.name@@", context.getName());
        
        String clientID_ = params.getValueParam("clientId").getValue();
        String clientSecret_ = params.getValueParam("clientSecret").getValue();
        if (redirectURL_ == null || redirectURL_.length() == 0 || clientID_ == null
            || clientID_.length() == 0 || clientSecret_ == null || clientSecret_.length() == 0) {
          throw new IllegalArgumentException("redirectURL, clientId and clientSecret must not be empty!");
        }
        this.redirectURL = redirectURL_;
        this.clientID = clientID_;
        this.clientSecret = clientSecret_;
        this.chunkLength = OAuthPersistenceUtils.getChunkLength(params);
        this.secureRandomService = secureRandomService;
      }

    The framework that manages the interaction with the GitHub servers will call the following method of the processor through the flow, passing it the request and response of each phase, and expecting an InteractionState in return.

      @Override
    
      public InteractionState<GithubAccessTokenContext> processOAuthInteraction(HttpServletRequest request, 
                                                                                HttpServletResponse response) throws IOException, OAuthException {
        HttpSession session = request.getSession();
        String state = (String) session.getAttribute(OAuthConstants.ATTRIBUTE_AUTH_STATE);
        
        // start the flow
        if (state == null || state.isEmpty()) {
          String verificationState = String.valueOf(secureRandomService.getSecureRandom().nextLong());
          initialInteraction(request, response, verificationState);
          state = InteractionState.State.AUTH.name();
          session.setAttribute(OAuthConstants.ATTRIBUTE_AUTH_STATE, state);
          session.setAttribute(OAuthConstants.ATTRIBUTE_VERIFICATION_STATE, verificationState);
          return new InteractionState<GithubAccessTokenContext>(InteractionState.State.valueOf(state), null);
        }
        
        // get access token
        if (state.equals(InteractionState.State.AUTH.name())) {
          //
          String accessToken = getAccessToken(request, response);
          GithubAccessTokenContext tokenContext = new GithubAccessTokenContext(accessToken);
          return new InteractionState<GithubAccessTokenContext>(InteractionState.State.FINISH, tokenContext);
        }
        return new InteractionState<GithubAccessTokenContext>(InteractionState.State.valueOf(state), null);
      }

    The access token persits in the (eXo) user profile. The following methods are called to save, get and remove an access token:

      @Override
    
      public void saveAccessTokenAttributesToUserProfile(UserProfile userProfile, OAuthCodec codec, GithubAccessTokenContext accessToken) {
        String encodedAccessToken = codec.encodeString(accessToken.getAccessToken());
        OAuthPersistenceUtils.saveLongAttribute(encodedAccessToken, userProfile, PROFILE_GITHUB_ACCESS_TOKEN, false, chunkLength);
      }
      
      @Override
      public GithubAccessTokenContext getAccessTokenFromUserProfile(UserProfile userProfile, OAuthCodec codec) {
        String encodedAccessToken = OAuthPersistenceUtils.getLongAttribute(userProfile, PROFILE_GITHUB_ACCESS_TOKEN, false);
        if (encodedAccessToken == null) {
          return null;
        }
        String accessToken = codec.decodeString(encodedAccessToken);
        return new GithubAccessTokenContext(accessToken);
      }
      
      @Override
      public void removeAccessTokenFromUserProfile(UserProfile userProfile) {
        OAuthPersistenceUtils.removeLongAttribute(userProfile, PROFILE_GITHUB_ACCESS_TOKEN, true);
      }
  4. Write a Filter, called GithubFilter, that extends the abstract filter org.gatein.security.oauth.web.OAuthProviderFilter.

    public class GithubFilter extends OAuthProviderFilter<GithubAccessTokenContext> {
    
    
    }

    This must implement the following three methods, in which the last one is called when the authorization is finished. You obtained an access token to get the GitHub user profile and return the user attributes wrapped into an OAuthPrincipal object. You can use any preferred libraries here to get the profile resouces. This tutorial simply uses java.net.HttpURLConnection and org.json.JSONObject.

    
    
      @Override
      protected OAuthProviderType<GithubAccessTokenContext> getOAuthProvider() {
        return this.getOauthProvider("GITHUB", GithubAccessTokenContext.class);
      }
      @Override
      protected void initInteraction(HttpServletRequest request, HttpServletResponse response) {
        HttpSession session = request.getSession();
        session.removeAttribute(OAuthConstants.ATTRIBUTE_AUTH_STATE);
        session.removeAttribute(OAuthConstants.ATTRIBUTE_VERIFICATION_STATE);
      }
      @Override
      protected OAuthPrincipal<GithubAccessTokenContext> getOAuthPrincipal(HttpServletRequest request, HttpServletResponse response, InteractionState<GithubAccessTokenContext> interactionState) {
        GithubAccessTokenContext accessTokenContext = interactionState.getAccessTokenContext();
        String accessToken = accessTokenContext.getAccessToken();
        Map<String, String> params = new HashMap<String, String>();
        params.put(OAuthConstants.ACCESS_TOKEN_PARAMETER, accessToken);
        String location = new StringBuilder(GithubProcessorImpl.PROFILE_ENDPOINT_URL).append("?").append(OAuthUtils.createQueryString(params)).toString();
        try {
          URL url = new URL(location);
          HttpURLConnection connection = (HttpURLConnection)url.openConnection();
          HttpResponseContext responseContext = OAuthUtils.readUrlContent(connection);
          if (responseContext.getResponseCode() == 200) {
            return parsePrincipal(responseContext.getResponse(), accessTokenContext, this.getOAuthProvider());
          } else {
            String errorMessage = "Unspecified IO error. Http response code: " + responseContext.getResponseCode() + ", details: " + responseContext.getResponse();
            throw new OAuthException(OAuthExceptionCode.IO_ERROR, errorMessage);
          }
        } catch (JSONException e) {
          throw new OAuthException(OAuthExceptionCode.IO_ERROR, e);
        } catch (IOException e) {
          throw new OAuthException(OAuthExceptionCode.IO_ERROR, e);
        }
      }
  5. Configure the service and filter to be loaded by the portal container, and register new provider as a plugin to the framework. While typically such configuration is placed in an extension, in this case it must be configured in a jar to be loaded before portal.war.

    
    <configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"
        xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd">
        <component>
            <key>org.exoplatform.extension.oauth.github.GithubProcessor</key>
            <type>org.exoplatform.extension.oauth.github.GithubProcessorImpl</type>
            <init-params>
                <value-param>
                    <name>clientId</name>
                    <value>${exo.oauth.github.clientId}</value>
                </value-param>
                <value-param>
                    <name>clientSecret</name>
                    <value>${exo.oauth.github.clientSecret}</value>
                </value-param>
                <value-param>
                    <name>redirectURL</name>
            <!--
            TODO: Should not expose property for this value.
            user will have hard configure with this value: 
            -->
                    <value>${exo.base.url:http://localhost:8080}/@@portal.container.name@@/githubAuth</value>
                </value-param>
                <!-- The custom scope is not supported so far, so don't edit the below -->
                <value-param>
                    <name>scope</name>
                    <value>${exo.oauth.github.scope:user}</value>
                </value-param>
            </init-params>
        </component>
      <component>
        <type>org.exoplatform.extension.oauth.github.GithubFilter</type>
        <init-params>
          <value-param>
            <!-- Value of this key must the same with value of key when configure OauthProviderTypeRegistryPlugin (line 79) -->
            <name>providerKey</name>
            <value>GITHUB</value>
          </value-param>
        </init-params>
      </component>

        <external-component-plugins>
            <target-component>org.gatein.security.oauth.webapi.OAuthFilterIntegrator</target-component>
            <component-plugin>
                <name>GithubFilter</name>
                <set-method>addPlugin</set-method>
                <type>org.gatein.security.oauth.webapi.OAuthFilterIntegratorPlugin</type>
                <init-params>
                    <value-param>
              <!-- Value of this key must the same with value of key when configure OauthProviderTypeRegistryPlugin (line 79) -->
                        <name>providerKey</name>
                        <value>GITHUB</value>
                    </value-param>
                    <value-param>
                        <name>filterClass</name>
                        <value>org.exoplatform.extension.oauth.github.GithubFilter</value>
                    </value-param>
                    <value-param>
                        <name>enabled</name>
                        <value>${exo.oauth.github.enabled:false}</value>
                    </value-param>
                    <value-param>
                        <name>filterMapping</name>
                        <value>/githubAuth</value>
                    </value-param>
                </init-params>
            </component-plugin>
        </external-component-plugins>

        <external-component-plugins>
            <target-component>org.gatein.security.oauth.spi.OAuthProviderTypeRegistry</target-component>
            <component-plugin>
                <name>GithubOauthProvider</name>
                <set-method>addPlugin</set-method>
                <type>org.gatein.security.oauth.registry.OauthProviderTypeRegistryPlugin</type>
                <init-params>
                    <value-param>
                        <name>key</name>
                        <value>GITHUB</value>
                    </value-param>
                    <value-param>
                        <name>enabled</name>
                        <value>${exo.oauth.github.enabled:false}</value>
                    </value-param>
                    <value-param>
                        <name>userNameAttributeName</name>
                        <value>user.social-info.github.userName</value>
                    </value-param>
                    <value-param>
                        <name>oauthProviderProcessorClass</name>
                        <value>org.exoplatform.extension.oauth.github.GithubProcessor</value>
                    </value-param>
                    <value-param>
                        <name>initOAuthURL</name>
                        <value>/githubAuth</value>
                    </value-param>
                    <value-param>
                        <name>friendlyName</name>
                        <value>GitHub</value>
                    </value-param>
                </init-params>
            </component-plugin>
        </external-component-plugins>
    </configuration>
  6. Finish the service module by adding the language resource in locale/portal/webui_en.properties:

    word.githubUsername=GitHub User Name
    # Used in AccountPortlet (when registering new user).
    UIAccountForm.label.user.social-info.github.userName=#{word.githubUsername}:
    
    # Used in OrganizationPortlet (when editting a profile from the Administration menu).
    UIUserInfo.label.user.social-info.github.userName=#{word.githubUsername}:
    
    # Used when a user edits his profile from Settings menu.
    UIAccountSocial.label.user.social-info.github.userName=#{word.githubUsername}:

    In UI when viewing a user profile, these keys are used to label the GitHub account fields. See the inline comment.

  7. Add the stylesheet in the file login.css under extension module:

    .uiLogin .loginContent #social-pane #social-login a .github {
    	background-image: url("/github-oauth-extension/skin/githubIcon.png");
    	width: 37px;
    	height: 35px;
    	display: block;
    	background-repeat: no-repeat;
    }
    	.uiLogin .loginContainer .loginContent #social-pane #social-login a .github {
    		background-image: url("/github-oauth-extension/skin/githubIcon.png");
    		width: 37px;
    		height: 35px;
    		display: block;
    		background-repeat: no-repeat;
    	}

    This CSS is applied to the login page. The framework automatically adds elements with class "github" (lowercase) to the page, so the CSS selectors are fixed.

  8. Register the stylesheet resource in gatein-resources.xml:

    
    <gatein-resources xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.gatein.org/xml/ns/gatein_resources_1_3 http://www.gatein.org/xml/ns/gatein_resources_1_3"
        xmlns="http://www.gatein.org/xml/ns/gatein_resources_1_3">
        <portlet-skin>
            <application-name>portal</application-name>
            <portlet-name>login</portlet-name>
            <skin-name>Default</skin-name>
            <css-path>/skin/login.css</css-path>
        </portlet-skin>
    </gatein-resources>

For the extension configuration and the packaging, refer to eXo Add-ons chapter.

Testing

Looking at your XML configuration in service module, the processor initialization requires the clientId and clientSecret. To test your addon, configure the file exo.properties (see Configuration overview for this file) like this:

exo.oauth.github.enabled=true
exo.oauth.github.clientId=3c7ca5b983626278703c
exo.oauth.github.clientSecret=6fa09a1e8d662914f80ec1a8389ae054065ceb40

The redirectURL parameter is generated based on exo.base.url property. You do not need to configure it while testing localhost:8080.

Copyright ©. All rights reserved. eXo Platform SAS
blog comments powered byDisqus