The JWT generated contains information in its payload about:
The expiring time
The post ID
The expiring time for the normal preview is 60 seconds, enough time to send the request to Frontity and Frontity send the request back to the REST API. After those 60 seconds it is not valid anymore.
A new token is generated for each request (each time the user clicks on the Preview button). That is because each time the expiring time changes, that means that the payload is different and the token is different.
The expering time for the publicly sharable link is infinite. That means that the token is always the same. To avoid having to save token in the database, just a post meta "public-share" setting is saved. The non-expiring token is only valid if that post meta is true. Disabling the sharable link simply turns the "public-share" meta to false.
The secret key used is a constant that the user needs to define in wp-config.php named PREVIEW_AUTH_KEY but it defaults to SECURE_AUTH_KEY if missing.
SUMMARY
Please, bear in mind that this section wasn’t part of the opening post. It has been added afterwards and its purpose is to keep a quick summary with the most relevant information about this FD at the top of the thread.
I started with the research, and the first thing I tried to solve was how to allow a REST API request to get revisions from a specific post, mocking the content of a JWT.
My first approach was to use the rest_authenticate_errors filter hook to check the JWT and authenticate somehow but that didn’t work because we don’t want authentication but authorization as the JWT won’t be associated to any WordPress user (correct me here if I’m wrong).
Later I found a filter called user_has_cap that allows WordPress to modify capabilities for a user during runtime, and I think it is a good solution. With that filter, you can allow any user you want (even the anonymous user if there’s no user authenticated) to access specific entities without any other check (I wrote the following code in my local environment and it works fine!). Only the entity specified in the mocked JWT can be fetched.
/**
* Modify user capabilities on run time.
*/
add_filter( 'user_has_cap', function ( $allcaps, $caps, $args, $user ) {
// Simulate the content of a JWT.
$jwt = array(
// Allow only GET requests so nothing can be modified or deleted.
'allow_methods' => array( 'GET' ),
// Capabilities needed to get revisions from the REST API.
'capabilities' => array( 'edit_post', 'delete_post' ),
// Post ID from which we want to get revisions.
'post_id' => 2003
);
// REST API check.
$is_rest_request = defined( 'REST_REQUEST' ) && REST_REQUEST;
// This is not a REST API request so do not change capabilities.
if ( ! $is_rest_request ) return $allcaps;
// TOKEN CHECKS.
// If it is not an allowed HTTP method do not change capabilities.
if ( ! in_array( $_SERVER[ 'REQUEST_METHOD' ], $jwt['allow_methods'] ) ) {
return $allcaps;
}
// If the capability being check doesn't match do not change capabilities.
if (! in_array( $args[0], $jwt['capabilities'] ) ) {
return $allcaps;
}
// If it is not the post ID do not change capabilities.
if ( $args[2] !== $jwt['post_id'] ) {
return $allcaps;
}
// Add capabilities.
foreach ( $caps as $cap ) {
$allcaps[ $cap ] = true;
}
// Return capabilities.
return $allcaps;
}, 9999, 4);
Using the idea I mentioned before, I modified the Frontity Embedded Mode - [Proof of Concept] in a way it renders the template.php file also for previews, sending a token to Frontity in order to get the latest review.
Right now, that token is generated using the PHP-JWT library, only last 60 seconds and contains information to allow any request – using that token in an Authorization: Bearer header – to only make HTTP GET requests with extended capabilities for a specific post or page (edit_post, delete_post). Those capabilities are needed when getting revisions from the REST API, not only for modifying them, but I think it is secure enough as we only allow HTTP GET requests by our implementation.
Regarding Frontity, I didn’t work on a standard way to store and handle tokens, I simply modified the postType handler to look for a token param in the link being fetched and, if it is found, fetches the latest revision using that token. When the response is received it replaces the content, title, and excerpt properties from the post by the received ones.
A thing that is not clear to me is where is the best place to generate tokens. At first, I thought of generating them using the preview_post_link hook but that would make the expiration countdown to start once you enter to the edit page (you may spend more than 60 seconds editing a post, right?). So, I decided to generate tokens when accessing a preview link (i.e. https://test.frontity.org/2016/the-beauties-of-gullfoss?preview=true&preview_id=60), but only when a user is logged in, preventing anonymous users to get preview tokens. Maybe there is a better way to do this so ideas are welcomed.
I guess we can check other things as well, sure. It only depends on our implementation. I used the Roles and Capabilities system WordPress already have for this but I didn’t take a deeper look into it yet. I also tried not to add user information in the tokens on purpose, but that’s another thing we can change if we find it appropriate.
This is great David ! I’ve tested it in my local environment and it works nicely It works exactly like WordPress, you’re redirected to the Frontity preview after clicking the button but the original post remains the same. You even keep the admin bar. Here you have a quick demo of the proof of concept:
We will have to modify the handler for the root path in order to recognize that kind of link.
I said that tokens last one minute (60 seconds) but they actually last 10 minutes long. I changed it for testing purposes and forgot to change it back.
Yes, that was my idea as well. That link needs to work always, not matter if I just edited the content or it was three days ago. For that, it needs to generate a new token each time it’s visited.
Not sure if it helps but if you are authenticated and you fetch the post X endpoint (not revisions) when it hasn’t been published yet, you could get the content for the preview. You even have the featured image or new categories, so it’d be like a common post.
@david, I don’t like that we rely on post._links["predecessor-version"] to fetch the preview post.
I analyzed a big REST API response and half of its size was due to the _links so removing that field means half the size of the response. If we rely on _links for this, people would not be able to remove them.
Do you know if there is a case where http://embedded.local/wp-json/wp/v2/posts/1/revisions/?per_page=1 is not good enough?
The solution proposed is to send both route (which is the link without the /page/x part) along with the query to the getMatch function, but only match the query for handlers that have a flag, for example, matchQuery: true.
The only thing I’ve not managed to figure out is how to set isHome. I’ll work on that and I’ll try to research the implications of adding this system to the current version of source.
The problem is that /?p=ID is not the home, but /?utm_campaign=sale is. We need to be able to differentiate between them.
I see two solutions for the isHome problem:
Add a whitelist of queries that are used in URLs to distinguish between.
Move the logic to the handlers.
Sadly, the second one is not backward-compatible because any person who has a custom handler for the home and it’s relying on data.isHome in his/her theme will be affected if we remove it.
Regarding handlers, I’m thinking about these two options:
Add support for preview to the postType handlers and add a new hander for ?p=ID.
Add a new handler for ?preview=true that handles both cases: slug (/some-post) and query (?p=ID).
As I mentioned in the PR, I think we should change the name of the token. token is too general. It should start at least with frontity_ to avoid collisions and include more information to allow for other types of tokens and versions.
I’m thinking:
Starts with frontity because all Frontity related queries should start like that.
It’s followed by preview because that its purpose.
It’s followed by jwt because that’s the implementation.
It ends with a version, in case we need to increase it at some point while maintaining the old implementation.
So something like this: frontity_preview_jwt_token_v1.