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.
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 integration can be turned on/off by configuration.
The login page must adapt to a turned-on provider, for example CSS and JavaScript are generated automatically.
When a user logs in using a Social network account, his profile data is propagated into eXo.
Access token persists for re-using. Revocation and expiration are handled.
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.
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:
Variable | Description |
---|---|
authentication endpoint |
|
access token endpoint |
|
profile endpoint |
|
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:
service contains Java classes, service configuration and translation resources. This module is packaged in jar.
extension is a webapp (war) containing stylesheet configuration and resources.
packaging module that packages the jar and the war in a zip to satisfy the Addon packaging requirement.
For Java code, the framework requires you to write:
An access token wrapper.
A processor that handles the interaction with GitHub.
A servlet filter.
Let's start your coding.
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;
}
}
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> {
}
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);
}
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);
}
}
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>
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.
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.
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.