Skip to content

Instantly share code, notes, and snippets.

@victorjonsson
Created September 26, 2014 14:21
Show Gist options
  • Save victorjonsson/75ec1af4378fd14cef9f to your computer and use it in GitHub Desktop.
Save victorjonsson/75ec1af4378fd14cef9f to your computer and use it in GitHub Desktop.

PayGate

WordPress + Varnish + External Login Service

This plugin makes the following assumptions about the login service:

  • The login service uses JWT
  • The signature in the user cookie provided by the login service contains an array of products that the user owns.
  • The user cookie provided by the login service contains a user ID, this id is also present in the cookie signature.
  • The login service has a javascript API.

Documentation sections

Requirements

  • WordPress v >= 3.5
  • PHP v >= 5.2
  • Support for URL rewrites (eg. htaccess module for apache)
  • That the permalink structure looks like /%post_id%/%postname%
  • Varnish >= 2.0

Installation

1) Upload plugin directory to server

2) Create a file named wp-config-paygate.php in the root directory of WordPress. Add the following constants to wp-config-paygate.php:

  • PAYGATE_JWT_SECRET Secret string provided by the login service
  • PAYGATE_WP_SECRET Random string used for internal encryption made by the plugin (validating ESI requests and access-tokens)
  • PAYGATE_ORG Organisation registered at the login service
  • PAYGATE_AUTH_URI Requested by the login service (http://yourdomain.se/wp-content/plugins/paygate/auth.php)
  • PAYGATE_CLIENT_ID ID of your account at the login service
  • PAYGATE_ENV test, stage or production (used by login service)
  • PAYGATE_JWT_COOKIE The name of the cookie created by the login server (T_ID)
  • PAYGATE_CACHE_PURGE_FUNC Name of function that can purge an URL on cache server (set to false if no such function exists)
  • PAYGATE_END_POINT For example vk.portal.worldoftulo.com
  • PAYGATE_CUSTOM_LOGIN Whether or not you want the plugin to render a login form on your website or redirect to the login service (more info below)
  • PAYGATE_EPAPER_END_POINT Base URL of the e-paper service, eg vasterbottenskuriren.e-pages.dk
  • PAYGATE_BUY_URI Address to page where you present your digital products
  • PAYGATE_HTTP_PROTOCOL Protocol used by the login service
  • PAYGATE_PRODUCT The name of the product that the user has to have to get access to content protected by the paygate
  • PAYGATE_PRODUCT_EPAPER The name of the e-paper product (can be comma separated strings with products)
  • PAYGATE_JS_HOST Host of the javascripts provided by the login service
  • PAYGATE_PRODUCT_LOYALTY_PROGRAM The name of loyalty program product

Optional constants:

  • PAYGATE_CONTENT_RESTRICTION_EXCLUDE Comma separated string with contexts where the_content and the_title filter shouldn't be constrained by the paygate
  • PAYGATE_OAUTH2_END_POINT Only needed if using custom login, eg. backend.worldoftulo.com
  • PAYGATE_ALLOW_ACCESS_BY_IP Turns on the possibility to bypass the paygate if having a certain ip address
  • PAYGATE_ALLOWED_USER_AGENTS A comma separated string with user agents that should bypass the paygate
  • PAYGATE_ALLOWED_IP_SERIES Let all clients from a certain IP-series bypass the paygate
  • PAYGATE_ALLOWED_IP_ADDRESSES List with ip addresses that should get access to paygate protected content without being logged in
  • PAYGATE_URI_PREFIX Use this constant to define your own prefix, instead of "plus".
  • PAYGATE_FEED_ITEM_FLAG Will be added to the beginning of titles in the RSS-feed (only if the post is protected by the paygate)
  • PAYGATE_ENABLE_TOGGLE_SWITCH Toggle whether or not to use a meta box for the paygate toggle or add a select to the meta box labeled "Published" (set to false if the toggle should have a meta-box of its own)
  • PAYGATE_ACCESS_TOKEN_SECRET A string used when encrypting access tokens used to create open urls (notice that the access token may not contain semicolons)
  • PAYGATE_LONGER_OPEN_URLS_PRODUCTS Comma separated string with product names that the user has to have (one of) in order to create open urls that's valid for sevens days instead of one day
  • PAYGATE_USER_SETTINGS_DIR Path where to store user settings files
  • PAYGATE_USER_SETTINGS_URL URL where user settings files can be accessed
  • PAYGATE_CONCURRENT_SESS_LIMIT The number of concurrent sessions that is allowed per user account.
  • PAYGATE_FRAUD_SESS_LIMIT The number of concurrent sessions that indicates the account is compromised
  • PAYGATE_CONCURRENT_SESS_PRODS The products that we track concurrent sessions for (omit to track all)

Notice! You must include wp-config-paygate.php at the very beginning of index.php if you define your own PAYGATE_URI_PREFIX

3) In wp-admin change permalink structure to /%post_id%/%postname%

4) Install plugin "paygate" in wp-admin

5) In top of index.php require the file init.php, located in the directory of this plugin. If you would like to change the URI prefix from "plus" to something else you should add the constant PAYGATE_URI_PREFIX to wp-config-local.php and include wp-config-local.php before including the init file in index.php

Varnish setup

ESI should be enabled if the URL contains the URI prefix "plus" (or the prefix you have defined yourself using the constant PAYGATE_URI_PREFIX) and the request has the cookie from the login service (PAYGATE_JWT_COOKIE) example:

sub vcl_recv {

    # No caching of paygate articles if user is logged in
    if (req.url ~ "^/plus" && req.http.cookie ~ "(^|;\s*)T_ID=(.*)active%22%3Atrue([^;])*") {
        return(pass);
    }
}

sub vcl_fetch {

    # Turn on ESI on articles behind paygate
    if (req.url ~ "^/plus" && req.http.cookie ~ "(?i)(T\_ID*)") {
        set beresp.do_esi = true;
        return(hit_for_pass);
    }
    ...
}

IP white-list

You can give emediate access to certain ip addresses and/or user agent by adding the following to wp-config-paygate.php. When your browser can bypass the paygate you can add ?hit-wall=1 to the URL if wanting to turn off the IP/UserAgent authentication.

// Turn on ip bypass
define('PAYGATE_ALLOW_ACCESS_BY_IP', true);

// Give emediate access to some clients.
define('PAYGATE_ALLOWED_IP_ADDRESSES', '
    -- Company X
    112.254.6.13
    112.254.6.14
    -- Company Z
    11.22.43.5
');

// Give access to entire IP-series (omit the last part)
define('PAYGATE_ALLOWED_IP_SERIES', '
    -- Oklahoma University
    65.124.81
');

// Give access to all apps having a user agent containing the following strings
define('PAYGATE_ALLOWED_USER_AGENTS', 'Yajmoko Crawler, MyCompany iOS App');

You must also declare the same ip list in varnish and turn on ESI for those clients.

acl paygate_direct_access_clients {
    "112.254.6.13";
    "11.22.43.5";
}

acl paygate_user_agents {
    "Yajmoko Crawler";
    "MyCompany iOS App";
}

sub vcl_recv {

    # Do not cache paygate articles under certain circumstances
    if (req.url ~ "^/plus" && req.http.cookie ~ "(^|;\s*)T_ID=(.*)active%22%3Atrue([^;])*" ||
        client.ip ~ paygate_direct_access_clients ||
        req.http.User-Agent ~ paygate_user_agents ) {
        return(pass);
    }
}

sub vcl_fetch {

    # Turn on ESI on articles behind paygate
    if (req.url ~ "^/plus" && req.http.cookie ~ "(?i)(T\_ID*)" ||
        client.ip ~ paygate_direct_access_clients ||
        req.http.User-Agent ~ paygate_user_agents ) {
        set beresp.do_esi = true;
        return(hit_for_pass);
    }
    ...
}

Open locked articles using access tokens

You can generate URL:s to paygate protected posts that is accessible by anyone, not only logged in customers, by using the function get_open_paygate_permalink($permalink, $post_id). This requires that you turn on ESI on paygate protected posts when the request contains "pak=" and that your permalink structure contains the post ID. (This requires that you define the constant PAYGATE_ACCESS_TOKEN_SECRET)

sub vcl_recv {

    # No caching of paygate articles if user is logged in or has paygate access token in URL
    if (req.url ~ "^/plus") {
        if(req.http.cookie ~ "(^|;\s*)T_ID=(.*)active%22%3Atrue([^;])*" || req.url ~ "^/plus/(.*)[\?|\&]pak(.*)$") {
            return(pass);
        }
    }
}

sub vcl_fetch {

    # Turn on ESI on articles behind paygate
    if (req.url ~ "^/plus") {
        if (req.http.cookie ~ "(?i)(T\_ID*)" || req.url ~ "^/plus/(.*)[\?|\&]pak(.*)$") {
            set beresp.do_esi = true;
            return(hit_for_pass);
        }
    }
    ...
}

Theme implementation

If you want to enable the login service in your theme you must add an element with ID "paygate-container" to your document. This plugin will add login/logout links to this element when the page gets rendered.

The login service will be requested at an early stage of the page rendering phase. Once the service returned a response with info about current logged in user a jQuery event will be triggered. You can use this event to make modifications to the theme once the page knows whether or not the client is a logged in user. Example

$(window).on('paygateLoaded', {

    if( PayGateUser ) {
        // user is logged in
        console.log(PayGateUser);

        if( PayGateUser.hasWebAccess() ) {
            // user has access to the content behind the paygate
        }
    }

});

The PayGateUser object

The object PayGateUser has in addition to the information provided by the login service also the following method ands properties:

PayGateUser.hasWebAccess() : Boolean — Whether or not the user owns the product that gives access to protected web content (constant PAYGATE_PRODUCT)

PayGateUser.hasLoyaltyProgramAccess() : Boolean — Whether or not the user should have access to the loyalty program

PayGateUser.hasEpaperAccess() : Boolean — Whether or not the user owns the product that gives access to the e-paper

PayGateUser.ePaperURL() : String — URL to where the e-paper can be downloaded

PayGateUser.hasProduct(prod) : Boolean — Whether or not the user has given product

PayGateUser.logout( [redirectURL] ) — Logout user from login service. The client gets redirected to the same URL if redirectURL is omitted

PayGateUser.settings() : Object — See below

PayGateUser.validatedByLoginService : Boolean — This variable will be false until the login service has validated the JWT cookie

PayGateUser.takeOverSession() — Used when monitoring concurrent sessions

If the login service provides information about gender and age this will be accessible via the global variables userGender userBirthYear and ageGroup. Notice that this information might be available even though the user isn't logged in.

User settings

The settings-object makes it possible to store arbitrary data belonging to the user. This information is saved as a static JSON-formatted file on the server. Because of this you should never store any information that might be harmful for the user in case it gets in the hands of a third party.

PayGateUser.settings().get(name) — Get a setting property

PayGateUser.settings().set(name, value, callback) — Save a setting property

PayGateUser.settings().savePost(id, callback) — Save an article that will be accessible in the array settings().get('articles'). Using this function minimizes the risk of data corruption

PayGateUser.settings().removePost(id, callback) — Removes a saved article. Using this function minimizes the risk of data corruption

PayGateUser.settings().loadPosts(callback) — Load WordPress posts saved by calling settings().savePost()

PayGateUser.settings().hasPosts() — Tells whether or not the user has saved posts.

PayGateUser.settings().numPosts() — Gives you the number of posts saved by the user.

PayGateUser.settings().load() — Reload all settings data from backend (note that the settings data will be available as soon as the event paygateLoaded has past)

Every time the settings data is reloaded from back-end a jQuery event named userSettingsLoaded is triggered. The settings is reloaded when a new setting property is saved to back-end or when you call PayGateUser.settings().load().

$(window).on('paygateLoaded', {

    if( PayGateUser ) {

        var renderSettingsMenu = function() {...};

        // Render settings menu immediately and when settings is reloaded from back-end
        renderSettingsMenu();
        $(window).on('userSettingsLoaded', renderSettingsMenu);
    }

});

Detecting concurrent user sessions

You can detect when there's several users logged in at the same time, on the same account, by listening to the event paygateConcurrentSessions. Use the constant PAYGATE_CONCURRENT_SESS_LIMIT and PAYGATE_FRAUD_SESS_LIMIT to tell the plugin when the js-event should be triggered.

$(window).bind('paygateConcurrentSessions', function(evt, numSessions, isFraud, fraudCause) {

    var $popup = $('<div id="concurrent-session"><p>There\'s '+numSessions+' active clients on this account</p></div>');

    if( isFraud ) {
        // Strange amount of concurrent sessions or to much swapping between owners of the master session
        $popup.append('<p>We\'re suspecting account fraud. Please wait a while and login again.</p>')
    } else {

        // Add button for taking over the login session
        $('<button>Kick them out!</button>').appendTo($popup).click(function() {
            PayGateUser.takeOverSession();
            $popup.remove();
        });

        // Add logout button
        $('<button>Log out</button>').appendTo($popup).click(function() {
            PayGateUser.logout();
        });
    }

});

Custom login form

If you want to use a custom login form (instead of redirecting the client to the login service) you must add an element with id "paygate-login" to your document. When the user clicks on the login link the login form will be rendered in the element with id "paygate-login". Clicking on the login link will also execute a function named "toggleCustomLoginForm", this function must be declared by the theme. Example:

var $loginForm = $('#paygate-login');
function toggleCustomLoginForm( isFormShowing, $link ) {
   if( isFormShowing ) {
    $loginForm.parent().hide();
    $link.text('Logga in');
   } else {
    $loginForm.parent().show();
    $link.text('Dölj inlogningsformulär');
   }
}

WordPress actions

  • paygate_post_activated When a post is put behind the paygate
  • paygate_post_deactivated When a post was behind the paygate and now is moved in front of the paygate
  • paygate_lockable_post_types Use this filter to lock other types of posts than "post" and "page"

XML resources protected by the paygate

This plugin also gives you the possibility to create XML feeds protected by the paygate. Use the hook "paygate_feed_request" and the function "paygate_protected_resource" to setup your own XML resource.

<?php
add_action('paygate_feed_request', function() {

    // The following feed will be available at /plus/paygate-feed/my-feed
    paygate_protected_resource(
        'my-feed',
        function( $authorized ) {
            if( $authorized )
                return '<status>Logged in</status>';
            else
                return '<status>Inte inloggad</status>';
        }
    );

});

The callback function should only return the body part of your XML document, it will be inserted in the XML document generated by the paygate. <?xml version="2.0"?><paygate authenticated="yes"><status>Logged in</status></paygate>

How to load your feed using ajax:

 $.ajax({
   url : '/plus/paygate-feed/my-feed',
   headers : {
      Accept : 'application/paygate+xml'
   },
   success : function(xml) {
     // do some stuff
   },
   errorCodes : {
      401 : function() {
         // Client is not logged in
      }
   }
 })

Changelog

1.4.4

  • Made user gender and age group available via js. Fixes for concurrent session monitoring

1.4.0

  • Added method hasProduct() to PayGateUser object
  • Now possible to track concurrent sessions
  • Now possible to have several e-paper products

1.3.7

  • Fixed bugs in PayGateUser.settings()
  • Added method for loading articles saved as settings

1.3.6

  • Now possible to store arbitrary data belonging to the logged in user (user settings)

1.3.5

  • Now possible to create access-tokens that can be used to unlock paygate protected posts
  • Constant PAYGATE_ESI_SECRET is renamed to PAYGATE_WP_SECRET

1.3.4

  • Now possible to give certain IP-addresses emediate access to paygate protected posts, without being logged in
  • Now possible to create XML resources protected by the paygate
  • Now possible to define your own URI prefix for content protected by the paygate

1.3.2

  • Support for different http protocols at login service
  • Fixed bug for custom login

1.3.1

  • Fixed minor bugs
  • Added actions for different events taking place in the plugin
  • Added some configuration constants

1.3.0

  • Added lock icon in arlima preview if plugin Arlima is installed

1.2.0

  • Added lock icons to post search in wp-admin
  • Add the_lock function to paygate API
  • Added constants PAYGATE_CUSTOM_LOGIN (read install)
  • Added mor info to readme.txt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment