Commit bbd05665 authored by Fabio Montefuscolo's avatar Fabio Montefuscolo Committed by Guus der Kinderen

Add OpenFire plugin to authenticate users against a token generated by TikiWiki CMS

parent 5dcd7595
# Openfire TikiToken plugin
This project is a plugin for the [Openfire Realtime Collaboration Server](http://www.igniterealtime.org/projects/openfire/).
This plugin adds a SASL mechanism that allows users to authenticate against a token that is generated by an instance of [Tiki Wiki CMS GroupWare](https://tiki.org/).
## Building the source
To create an Openfire plugin from the source code in this project:
- Download a copy of the sources of this project into `PLUGINDIR`
- Download a copy of the [Openfire sources](https://github.com/igniterealtime/Openfire) into `OPENFIRE_SOURCE_DIR`
- From within `OPENFIRE_SOURCE_DIR` execute: `ant -f build/build.xml -Dplugin.src.dir=PLUGINDIR/.. -Dplugin=tikitoken openfire plugin`
## Installing the plugin
After a plugin JAR file has been created, place that file in the plugin directory of a running Openfire instance.
Openfire will automatically detect and activate the plugin.
More detailed usage instructions are available in the [readme](readme.html) file that is available as part of the source
code of this project.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>TikiToken Plugin Changelog</title>
<style type="text/css">
BODY {
font-size : 100%;
}
BODY, TD, TH {
font-family : tahoma, verdana, arial, helvetica, sans-serif;
font-size : 0.8em;
}
H2 {
font-size : 10pt;
font-weight : bold;
}
A:hover {
text-decoration : none;
}
H1 {
font-family : tahoma, arial, helvetica, sans-serif;
font-size : 1.4em;
font-weight: bold;
border-bottom : 1px #ccc solid;
padding-bottom : 2px;
}
TT {
font-family : courier new;
font-weight : bold;
color : #060;
}
PRE {
font-family : courier new;
font-size : 100%;
}
</style>
</head>
<body>
<h1>TikiToken Plugin Changelog</h1>
<p><b>0.2</b> -- March 21, 2017</p>
<ul>
<li>Replaced AuthProvider-based approach by a SASL mechanism-based approach.</li>
</ul>
<p><b>0.1</b> -- March 19, 2017</p>
<ul>
<li>Initial release</li>
</ul>
</body>
</html>
\ No newline at end of file
<plugin>
<class>org.tiki.tikitoken.TikiTokenPlugin</class>
<name>TikiToken</name>
<description>Allows users to authenticate with a Tiki token.</description>
<author>Tiki Wiki CMS GroupWare</author>
<version>0.2</version>
<date>04/24/2017</date>
<url>https://dev.tiki.org/OpenFire</url>
<minServerVersion>4.1.3</minServerVersion>
<licenseType>gpl</licenseType>
</plugin>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>TikiToken Plugin Readme</title>
<style type="text/css">
BODY {
font-size : 100%;
}
BODY, TD, TH {
font-family : tahoma, verdana, arial, helvetica, sans-serif;
font-size : 0.8em;
}
H2 {
font-size : 10pt;
font-weight : bold;
}
A:hover {
text-decoration : none;
}
H1 {
font-family : tahoma, arial, helvetica, sans-serif;
font-size : 1.4em;
font-weight: bold;
border-bottom : 1px #ccc solid;
padding-bottom : 2px;
}
TT {
font-family : courier new;
font-weight : bold;
color : #060;
}
PRE {
font-family : courier new;
font-size : 100%;
}
</style>
</head>
<body>
<h1>
TikiToken Plugin Readme
</h1>
<h2>Overview</h2>
<p>
This plugin adds a SASL mechanism to Openfire that allows users to authenticate with a Tiki-generated access token.
</p>
<h2>Installation</h2>
<p>
Copy the plugin JAR file into the plugins directory of your Openfire installation. The plugin will then be
automatically deployed.
</p>
<h2>Upgrade</h2>
<p>
To upgrade to a new version, copy the new plugin JAR file into the plugins directory of your Openfire installation,
overwriting the JAR file from the previous version. Within a minute or so, Openfire will detect the new plugin,
unload the previous version and load the new version. You can verify that the new version has been loaded by
observing the plugin version number in the Openfire Admin Console.
</p>
<h2>Configuration</h2>
<p>
The base URL of the Tiki suite against which authentication is performed needs to be defined in a property named
<tt>org.tiki.tikitoken.baseUrl</tt>
</p>
</body>
</html>
package org.tiki.tikitoken;
import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.net.SASLAuthentication;
import java.io.File;
import java.security.Security;
/**
* An Openfire plugin that adds the TikiToken SASL mechanism.
*/
public class TikiTokenPlugin implements Plugin
{
@Override
public void initializePlugin( PluginManager manager, File pluginDirectory )
{
Security.addProvider( new TikiTokenSaslProvider() );
SASLAuthentication.addSupportedMechanism( TikiTokenSaslServer.MECHANISM_NAME );
}
@Override
public void destroyPlugin()
{
SASLAuthentication.removeSupportedMechanism( TikiTokenSaslServer.MECHANISM_NAME );
Security.removeProvider( TikiTokenSaslProvider.NAME );
}
}
package org.tiki.tikitoken;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;
import java.util.Scanner;
import org.jivesoftware.util.JiveGlobals;
import com.owlike.genson.Genson;
public class TikiTokenQuery {
private final String DEFAULT_BASE_URL = "http://tikiconverse.docker/";
private String username;
private String token;
public TikiTokenQuery(String username, String token) {
this.username = username;
this.token = token;
}
public URL getUrl() throws MalformedURLException, URISyntaxException {
String baseAddress = JiveGlobals.getProperty("org.tiki.tikitoken.baseUrl", this.DEFAULT_BASE_URL);
String script = String.format("tiki-ajax_services.php?controller=xmpp&action=check_token&user=%s&token=%s", this.username, this.token);
URL baseUrl = new URL(baseAddress);
URL fullUrl = new URL(baseUrl, script);
return fullUrl;
}
public boolean isValid() {
String content = this.fetch();
Genson genson = new Genson();
Map<String, Boolean> root = genson.deserialize(content, Map.class);
return root.get("valid");
}
public String fetch() {
try {
URL url = this.getUrl();
InputStream stream = url.openStream();
Scanner s = new java.util.Scanner( stream ).useDelimiter( "\\A" );
String result = s.hasNext() ? s.next() : "";
s.close();
return result;
} catch (IOException | URISyntaxException e) {
e.printStackTrace();
}
return null;
}
}
package org.tiki.tikitoken;
import java.security.Provider;
/**
* A Provider implementation for a SASL mechanism that uses a Tiki token.
*
* This implementation makes use of a Tiki server for token validation.
*
* @see <a href="https://tools.ietf.org/html/rfc7628">RFC 7628</a>
*/
public class TikiTokenSaslProvider extends Provider
{
/**
* The provider name.
*/
public static final String NAME = "TikiSasl";
/**
* The provider version number.
*/
public static final double VERSION = 1.0;
/**
* A description of the provider and its services.
*/
public static final String INFO = "Provides a SASL mechanism that uses a Tiki instance to verify authentication tokens.";
public TikiTokenSaslProvider()
{
super( NAME, VERSION, INFO );
put( "SaslServerFactory." + TikiTokenSaslServer.MECHANISM_NAME, TikiTokenSaslServerFactory.class.getCanonicalName() );
}
}
\ No newline at end of file
package org.tiki.tikitoken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import java.nio.charset.StandardCharsets;
import java.util.StringTokenizer;
/**
* A SaslServer implementation of the TikiToken mechanism.
*
* @author Guus der Kinderen, guus@goodbytes.nl
*/
public class TikiTokenSaslServer implements SaslServer
{
private final static Logger Log = LoggerFactory.getLogger( TikiTokenSaslServer.class );
/**
* The mechanism name of this implementation: "TIKITOKEN"
*/
public static final String MECHANISM_NAME = "TIKITOKEN";
private enum State {
/** Initial state. Has not evaluated any response yet. */
PRE_INITIAL_RESPONSE,
/** Has evaluated an initial response, but has not yet completed. */
POST_INITIAL_RESPONSE,
/** Done (authentication succeeded or failed). */
COMPLETED
}
private String authorizationID = null;
private State state = State.PRE_INITIAL_RESPONSE;
public TikiTokenSaslServer()
{}
/**
* Returns the mechanism name of this SASL server: "TIKITOKEN".
*
* @return A non-null string representing the mechanism name: TIKITOKEN
*/
public String getMechanismName()
{
return MECHANISM_NAME;
}
public byte[] evaluateResponse( byte[] response ) throws SaslException
{
Log.trace( "Evaluating new response..." );
if( isComplete() )
{
throw new IllegalStateException( "TIKITOKEN authentication was already completed." );
}
Log.trace( "Current state: {}", state );
switch ( state )
{
case POST_INITIAL_RESPONSE:
if ( response.length == 0 )
{
state = State.COMPLETED;
throw new SaslException( "The TIKITOKEN SASL mechanism expects response data in either the initial or second client response. Neither had any data." );
}
// Intended fall-through.
case PRE_INITIAL_RESPONSE:
if ( response.length == 0 )
{
// No data in the initial response. Ask for data by responding with a success.
state = State.POST_INITIAL_RESPONSE;
return null;
}
else
{
// We have data: no further responses are expected.
state = State.COMPLETED;
Log.trace( "Parsing data from client response..." );
final String data = new String( response, StandardCharsets.UTF_8);
final StringTokenizer tokens = new StringTokenizer( data, "\0");
if ( tokens.countTokens() != 2 )
{
throw new SaslException( "Exactly two NUL (U+0000) character-separated values are expected (a username, followed by a Tiki access token). Instead " + tokens.countTokens() + " were found." );
}
final String username = tokens.nextToken();
final String tikiToken = tokens.nextToken();
Log.trace( "Parsed data from client response for user '{}'. Verifying Tiki token...", username );
final TikiTokenQuery query = new TikiTokenQuery( username, tikiToken );
if ( !query.isValid() )
{
throw new SaslException( "Tiki token based authentication failed for: " + username );
}
Log.debug( "Authentication successful for user '{}'!", username );
authorizationID = username;
return null;
}
default:
throw new IllegalStateException( "Instance is in an unrecognized state (please report this incident as a bug in class: " + this.getClass().getCanonicalName() + "). Unrecognized value: " + state );
}
}
public boolean isComplete()
{
return state == State.COMPLETED;
}
public String getAuthorizationID()
{
if( !isComplete() )
{
throw new IllegalStateException( "TIKITOKEN authentication has not completed." );
}
return authorizationID;
}
public Object getNegotiatedProperty( String propName )
{
if( !isComplete() )
{
throw new IllegalStateException( "TIKITOKEN authentication has not completed." );
}
if ( Sasl.QOP.equals( propName ) )
{
return "auth";
}
return null;
}
public void dispose() throws SaslException
{
state = null;
authorizationID = null;
}
public byte[] unwrap( byte[] incoming, int offset, int len ) throws SaslException
{
if( !isComplete() )
{
throw new IllegalStateException( "TIKITOKEN authentication has not completed." );
}
throw new IllegalStateException( "TIKITOKEN supports neither integrity nor privacy." );
}
public byte[] wrap( byte[] outgoing, int offset, int len ) throws SaslException
{
if( !isComplete() )
{
throw new IllegalStateException( "TIKITOKEN authentication has not completed." );
}
throw new IllegalStateException( "TIKITOKEN supports neither integrity nor privacy." );
}
}
package org.tiki.tikitoken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.auth.callback.CallbackHandler;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import javax.security.sasl.SaslServerFactory;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* A SaslServerFactory implementation that is used to instantiate TikiToken-based SaslServer instances.
*
* This implementation makes use of a Tiki server for token validation.
*
* @author Guus der Kinderen, guus@goodbytes.nl
*/
public class TikiTokenSaslServerFactory implements SaslServerFactory
{
private static final Logger Log = LoggerFactory.getLogger( TikiTokenSaslServerFactory.class );
public TikiTokenSaslServerFactory()
{
}
public SaslServer createSaslServer( String mechanism, String protocol, String serverName, Map<String, ?> props, CallbackHandler cbh ) throws SaslException
{
// Do not return an instance when the provided properties contain Policy settings that disallow our implementations.
final Set<String> mechanismNames = getMechanismNamesSet( props );
if ( mechanismNames.contains( mechanism ) && mechanism.equalsIgnoreCase( TikiTokenSaslServer.MECHANISM_NAME ) )
{
Log.debug( "Instantiating a new TikiTokenSaslServer instance." );
return new TikiTokenSaslServer();
}
Log.debug( "Unable to instantiate a SaslServer instance that matches the requested properties." );
return null;
}
public String[] getMechanismNames( Map<String, ?> props )
{
final Set<String> result = getMechanismNamesSet( props );
return result.toArray( new String[ result.size() ] );
}
protected final Set<String> getMechanismNamesSet( Map<String, ?> props )
{
final Set<String> supportedMechanisms = new HashSet<String>();
supportedMechanisms.add( TikiTokenSaslServer.MECHANISM_NAME );
if ( props != null )
{
for ( Map.Entry<String, ?> prop : props.entrySet() )
{
if ( !( prop.getValue() instanceof String ) )
{
continue;
}
final String name = prop.getKey();
final String value = (String) prop.getValue();
if ( Sasl.POLICY_NOPLAINTEXT.equalsIgnoreCase( name ) && "true".equalsIgnoreCase( value ) )
{
Log.info( "Removing '{}' mechanism, as the provided properties define a NOPLAINTEXT policy.", TikiTokenSaslServer.MECHANISM_NAME );
supportedMechanisms.remove( TikiTokenSaslServer.MECHANISM_NAME );
}
// TODO Determine if other policies are relevant.
}
}
return supportedMechanisms;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment