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.
- Requirements
- Installation
- Varnish setup
- IP white-list and giving access to certain user agents
- Open locked articles using access tokens
- Theme implementation
- Custom login form
- WordPress actions/filters
- XML resources protected by the paygate
- Changelog
- 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
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
- 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
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);
}
...
}
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);
}
...
}
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);
}
}
...
}
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 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.
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);
}
});
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();
});
}
});
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');
}
}
- 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"
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
}
}
})
- Made user gender and age group available via js. Fixes for concurrent session monitoring
- Added method hasProduct() to PayGateUser object
- Now possible to track concurrent sessions
- Now possible to have several e-paper products
- Fixed bugs in PayGateUser.settings()
- Added method for loading articles saved as settings
- Now possible to store arbitrary data belonging to the logged in user (user settings)
- 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
- 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
- Support for different http protocols at login service
- Fixed bug for custom login
- Fixed minor bugs
- Added actions for different events taking place in the plugin
- Added some configuration constants
- Added lock icon in arlima preview if plugin Arlima is installed
- 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