├── assets ├── icon-128x128.png ├── icon-256x256.png ├── banner-1544x500.png └── banner-772x250.png ├── docs ├── advanced │ ├── README.md │ ├── Desktop.md │ └── Web.md ├── introduction │ ├── README.md │ ├── Setup.md │ ├── OAuth.md │ └── OAuth-1.md ├── basics │ ├── README.md │ ├── Registering.md │ ├── Auth-Flow.md │ └── Signing.md ├── README.md └── spec.md ├── inc ├── class-scopes.php ├── tokens │ ├── namespace.php │ ├── class-token.php │ ├── class-access-token.php │ └── class-authorization-code.php ├── types │ ├── class-type.php │ ├── class-implicit.php │ ├── class-authorization-code.php │ └── class-base.php ├── endpoints │ ├── namespace.php │ ├── class-authorization.php │ └── class-token.php ├── class-clientinterface.php ├── admin │ ├── class-listtable.php │ ├── profile │ │ ├── personaltokens │ │ │ └── namespace.php │ │ └── namespace.php │ └── namespace.php ├── namespace.php ├── class-personalclient.php ├── authentication │ └── namespace.php └── class-client.php ├── book.json ├── .gitignore ├── phpunit.xml.dist ├── bin ├── readme.txt ├── release.sh └── install-wp-tests.sh ├── composer.json ├── tests ├── bootstrap.php └── install-tests.sh ├── .travis.yml ├── .phpcs.xml.dist ├── README.md ├── plugin.php ├── theme └── oauth2-authorize.php └── LICENSE.txt /assets/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WP-API/OAuth2/HEAD/assets/icon-128x128.png -------------------------------------------------------------------------------- /assets/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WP-API/OAuth2/HEAD/assets/icon-256x256.png -------------------------------------------------------------------------------- /assets/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WP-API/OAuth2/HEAD/assets/banner-1544x500.png -------------------------------------------------------------------------------- /assets/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WP-API/OAuth2/HEAD/assets/banner-772x250.png -------------------------------------------------------------------------------- /docs/advanced/README.md: -------------------------------------------------------------------------------- 1 | # Advanced 2 | 3 | * [Desktop/Mobile Clients](Desktop.md) 4 | * [Web Clients](Web.md) 5 | -------------------------------------------------------------------------------- /docs/introduction/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | * [Why OAuth?](OAuth.md) 4 | * [Why OAuth 1.0a?](OAuth-1.md) 5 | * [Setup](Setup.md) -------------------------------------------------------------------------------- /docs/basics/README.md: -------------------------------------------------------------------------------- 1 | # Basics 2 | 3 | * [Registering an Application](Registering.md) 4 | * [The Authorization Flow](Auth-Flow.md) 5 | * [Signing Requests](Signing.md) -------------------------------------------------------------------------------- /inc/class-scopes.php: -------------------------------------------------------------------------------- 1 | capabilities = []; 15 | } 16 | 17 | public function register( $id, $capabilities ) { 18 | $this->scopes[ $id ] = $capabilities; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /inc/tokens/namespace.php: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./tests/ 13 | ./tests/test-sample.php 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/introduction/Setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Before you can connect to your site with OAuth, you'll need to get it set up. 4 | 5 | Right now, you'll need to install the plugin [from GitHub](https://github.com/WP-API/OAuth1). Inside your plugin directory, clone the plugin down: 6 | 7 | git clone https://github.com/WP-API/OAuth1 8 | 9 | Once you've done this, head to your WordPress dashboard and activate the plugin. You should see an "Applications" item appear under the users menu: this is where you manage OAuth clients. -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | * [Start Here](/README.md) 4 | * [Introduction](/docs/introduction/README.md) 5 | * [Why OAuth?](/docs/introduction/OAuth.md) 6 | * [Why OAuth 1.0a?](/docs/introduction/OAuth-1.md) 7 | * [Setup](/docs/introduction/Setup.md) 8 | * [Basics](/docs/basics/README.md) 9 | * [Registering an Application](/docs/basics/Registering.md) 10 | * [The Authorization Flow](/docs/basics/Auth-Flow.md) 11 | * [Signing Requests](/docs/basics/Signing.md) 12 | * [Advanced](/docs/advanced/README.md) 13 | * [Desktop/Mobile Clients](/docs/advanced/Desktop.md) 14 | * [Web Clients](/docs/advanced/Web.md) 15 | -------------------------------------------------------------------------------- /bin/readme.txt: -------------------------------------------------------------------------------- 1 | === WordPress REST API - OAuth 2 Server === 2 | Contributors: rmccue, rachelbaker, danielbachhuber, joehoyle 3 | Tags: json, rest, api, rest-api 4 | Requires at least: 4.8 5 | Tested up to: 4.8 6 | Stable tag: {{TAG}} 7 | License: GPLv2 or later 8 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 9 | 10 | == Description == 11 | Connect applications to your WordPress site without ever giving away your password. 12 | 13 | This plugin uses the OAuth 2 protocol to allow delegated authorization; that is, to allow applications to access a site using a set of secondary credentials. This allows server administrators to control which applications can access the site, as well as allowing users to control which applications have access to their data. 14 | 15 | This plugin only supports WordPress >= 4.8. 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-api/oauth2", 3 | "description": "OAuth 2 Server for WordPress", 4 | "type": "wordpress-plugin", 5 | "license": "GPL2+", 6 | "authors": [ 7 | { 8 | "name": "WP-API Team", 9 | "homepage": "http://wp-api.org/" 10 | } 11 | ], 12 | "require": { 13 | "composer/installers": "~1.0", 14 | "php": "^5.6.0||^7.0||^8.0" 15 | }, 16 | "require-dev": { 17 | "squizlabs/php_codesniffer": "^3.3.1", 18 | "wp-coding-standards/wpcs": "^2.1.1", 19 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", 20 | "phpcompatibility/phpcompatibility-wp": "^2.0", 21 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0" 22 | }, 23 | "scripts": { 24 | "post-install-cmd": "\"vendor/bin/phpcs\" --config-set installed_paths vendor/wp-coding-standards/wpcs", 25 | "post-update-cmd" : "\"vendor/bin/phpcs\" --config-set installed_paths vendor/wp-coding-standards/wpcs" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | register_routes(); 20 | 21 | // Register convenience URL. 22 | register_rest_route( 23 | 'oauth2', 24 | '/authorize', 25 | [ 26 | 'methods' => 'GET', 27 | 'callback' => __NAMESPACE__ . '\\redirect_to_authorize', 28 | ] 29 | ); 30 | } 31 | 32 | /** 33 | * Handle authorize endpoint request. 34 | * 35 | * This endpoint exists as a convenience URL to avoid clients needing to find 36 | * wp-login.php. 37 | * 38 | * @param WP_REST_Request $request Request object. 39 | * @return WP_REST_Response Response object. 40 | */ 41 | function redirect_to_authorize( WP_REST_Request $request ) { 42 | $url = OAuth2\get_authorization_url(); 43 | 44 | $query = $request->get_query_params(); 45 | if ( ! empty( $query ) ) { 46 | // Pass query arguments along. 47 | $url = add_query_arg( 48 | urlencode_deep( $query ), 49 | $url 50 | ); 51 | } 52 | 53 | return new WP_REST_Response( [ 'url' => $url ], 302, [ 'Location' => $url ] ); 54 | } 55 | -------------------------------------------------------------------------------- /docs/introduction/OAuth.md: -------------------------------------------------------------------------------- 1 | # Why OAuth? 2 | 3 | When developing a REST API, there's no shortage of possible authentication options to connect to your site. These options span from simple username and password schemes up to much more complex systems. Why choose OAuth out of all of these? 4 | 5 | OAuth is built around a singular core concept: **delegated authorization**. Unlike traditional username and password systems, or even API keys, OAuth doesn't have a single set of credentials. Instead, it splits the concept of credentials into two: client credentials, and user tokens. Clients register with sites they want to access, but this doesn't give them any inherent access. Users then authorize the client to perform actions on their behalf. 6 | 7 | Decoupling these pieces gives better flexibility and security. If a client is compromised or accidentally leaks credentials, these can be revoked, disconnecting the client from all users. If a single user wants to disconnect the client, they can revoke the user token issued to the client. Combined, this gives both site owners and users control over their data. 8 | 9 | This also crucially avoids the anti-pattern of giving credentials to external applications. In particular, OAuth itself provides **no ability to exchange a username and password for a user token**. This reinforces that users should never give their username and password to other applications. This also helps mitigate phishing exploits. -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | 4 | language: php 5 | 6 | notifications: 7 | email: 8 | on_success: never 9 | on_failure: change 10 | 11 | branches: 12 | only: 13 | - master 14 | 15 | cache: 16 | directories: 17 | - $HOME/.composer/cache 18 | 19 | matrix: 20 | include: 21 | - php: 7.3 22 | env: WP_VERSION=latest 23 | - php: 7.2 24 | env: WP_VERSION=latest 25 | - php: 7.1 26 | env: WP_VERSION=latest 27 | - php: 7.0 28 | env: WP_VERSION=latest 29 | - php: 7.0 30 | env: WP_VERSION=trunk 31 | - php: 7.0 32 | env: WP_TRAVISCI=phpcs 33 | dist: precise 34 | 35 | before_script: 36 | - export PATH="$HOME/.composer/vendor/bin:$PATH" 37 | - composer install --ignore-platform-reqs --optimize-autoloader --no-interaction --prefer-dist 38 | - | 39 | if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then 40 | phpenv config-rm xdebug.ini 41 | else 42 | echo "xdebug.ini does not exist" 43 | fi 44 | - | 45 | if [[ ! -z "$WP_VERSION" ]] ; then 46 | bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION 47 | fi 48 | 49 | script: 50 | - | 51 | if [[ ! -z "$WP_VERSION" ]] ; then 52 | vendor/bin/phpunit 53 | WP_MULTISITE=1 vendor/bin/phpunit 54 | fi 55 | - | 56 | if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then 57 | vendor/bin/phpcs 58 | fi 59 | -------------------------------------------------------------------------------- /docs/introduction/OAuth-1.md: -------------------------------------------------------------------------------- 1 | # Why OAuth 1.0a? 2 | 3 | If you've done research on OAuth, you might notice OAuth 2.0 exists. Why is the canonical authorization scheme using an older version of OAuth? 4 | 5 | OAuth has a long and storied history behind it. The OAuth concept was born from Twitter (and others) needing delegated authorization for user accounts, primarily for API access. This then continued evolving with feedback from other parties (such as Google), before eventually being standardised as OAuth 1.0a in [RFC 5849](https://tools.ietf.org/html/rfc5849). OAuth was then further evolved and simplified in OAuth 2.0, standardised as [RFC 6749](https://tools.ietf.org/html/rfc6749). 6 | 7 | The primary change from version 1 to 2 was the removal of the complicated signature system. This signature system was designed to ensure only the client can use the user tokens, since it relies on a shared secret. However, every request must be individually signed. Version 2 instead relies on SSL/TLS to handle message authenticity. 8 | 9 | This means that **OAuth 2.0 requires HTTPS**. WordPress however does not. We need to be able to provide authentication for all sites, not just those with HTTPS. 10 | 11 | With the impending changes to the HTTPS playing field with the Let's Encrypt certificate authority, we hope to be able to require SSL in the future and move to OAuth 2.0, but this is not yet feasible. 12 | 13 | (Note: While the OAuth RFC requires SSL for some endpoints, [OAuth 1.0a](http://oauth.net/core/1.0a/) does not. This is a willful violation of the RFC, as we need to support non-SSL sites.) -------------------------------------------------------------------------------- /.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Generally-applicable sniffs for WordPress plugins. 4 | 5 | 6 | . 7 | /vendor/ 8 | /node_modules/ 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /bin/release.sh: -------------------------------------------------------------------------------- 1 | # release.sh 2 | # 3 | # Takes a tag to release, and syncs it to WordPress.org 4 | 5 | TAG=$1 6 | 7 | PLUGIN="rest-api-oauth2" 8 | TMPDIR=/tmp/rest-api-oauth2-release-svn 9 | PLUGINDIR="$PWD" 10 | PLUGINSVN="https://plugins.svn.wordpress.org/$PLUGIN" 11 | 12 | # Fail on any error 13 | set -e 14 | 15 | # Is the tag valid? 16 | if [ -z "$TAG" ] || ! git rev-parse "$TAG" > /dev/null; then 17 | echo "Invalid tag. Make sure you tag before trying to release." 18 | exit 1 19 | fi 20 | 21 | if [[ $VERSION == "v*" ]]; then 22 | # Starts with an extra "v", strip for the version 23 | VERSION=${TAG:1} 24 | else 25 | VERSION="$TAG" 26 | fi 27 | 28 | if [ -d "$TMPDIR" ]; then 29 | # Wipe it clean 30 | rm -r "$TMPDIR" 31 | fi 32 | 33 | # Ensure the directory exists first 34 | mkdir "$TMPDIR" 35 | 36 | # Grab an unadulterated copy of SVN 37 | svn co "$PLUGINSVN/trunk" "$TMPDIR" > /dev/null 38 | 39 | # Extract files from the Git tag to there 40 | git archive --format="zip" -0 "$TAG" | tar -C "$TMPDIR" -xf - 41 | 42 | # Switch to build dir 43 | cd "$TMPDIR" 44 | 45 | # Run build tasks 46 | sed -e "s/{{TAG}}/$VERSION/g" < "$PLUGINDIR/bin/readme.txt" > readme.txt 47 | 48 | # Remove special files 49 | rm ".gitignore" 50 | rm "composer.json" 51 | rm "book.json" 52 | rm -r "bin" 53 | rm -r "docs" 54 | 55 | # Add any new files 56 | svn status | grep -v "^.[ \t]*\..*" | grep "^?" | awk '{print $2}' | xargs svn add 57 | 58 | # Pause to allow checking 59 | echo "About to commit $VERSION. Double-check $TMPDIR to make sure everything looks fine." 60 | read -p "Hit Enter to continue." 61 | 62 | # Commit the changes 63 | svn commit -m "Tag $VERSION" 64 | 65 | # tag_ur_it 66 | svn copy "$PLUGINSVN/trunk" "$PLUGINSVN/tags/$VERSION" -m "Tag $VERSION" 67 | -------------------------------------------------------------------------------- /inc/endpoints/class-authorization.php: -------------------------------------------------------------------------------- 1 | get_response_type_code() === $type ) { 38 | $handler = $type_handler; 39 | break; 40 | } 41 | } 42 | } 43 | 44 | if ( empty( $handler ) ) { 45 | $result = new WP_Error( 46 | 'oauth2.endpoints.authorization.handle_request.invalid_type', 47 | __( 'Invalid response type specified.', 'oauth2' ) 48 | ); 49 | } else { 50 | $result = $handler->handle_authorisation(); 51 | } 52 | 53 | if ( is_wp_error( $result ) ) { 54 | // TODO: Handle it. 55 | wp_die( esc_html( $result->get_error_message() ) ); 56 | } 57 | exit; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth 2.0 for WordPress 2 | 3 | Connect applications to your WordPress site without ever giving away your password. 4 | 5 | This plugin uses the OAuth 2 protocol to allow delegated authorization; that is, to allow applications to access a site using a set of secondary credentials. This allows server administrators to control which applications can access the site, as well as allowing users to control which applications have access to their data. 6 | 7 | This plugin only supports WordPress >= 4.8. 8 | 9 | ## Contributors Welcome! 10 | 11 | This plugin works and is in use in several production environments, but the user experience and documentation could be substantially improved. We welcome input and contributions to make this tool better! 12 | 13 | 14 | ## Credits 15 | 16 | This plugin is licensed under the GNU General Public License v2 or later: 17 | 18 | > Copyright 2017 by the contributors. 19 | > 20 | > This program is free software; you can redistribute it and/or modify 21 | > it under the terms of the GNU General Public License as published by 22 | > the Free Software Foundation; either version 2 of the License, or 23 | > (at your option) any later version. 24 | > 25 | > This program is distributed in the hope that it will be useful, 26 | > but WITHOUT ANY WARRANTY; without even the implied warranty of 27 | > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 28 | > GNU General Public License for more details. 29 | > 30 | > You should have received a copy of the GNU General Public License 31 | > along with this program; if not, write to the Free Software 32 | > Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 33 | 34 | Thanks to the contributors at the WCEU 2017 Contributor Day who were responsible for getting this plugin off the ground and into a usable state: @almirbi, @richardsweeney, @tfrommen. 35 | -------------------------------------------------------------------------------- /inc/tokens/class-token.php: -------------------------------------------------------------------------------- 1 | user = $user; 36 | $this->key = $key; 37 | $this->value = $value; 38 | } 39 | 40 | /** 41 | * Get the ID for the user that the token represents. 42 | * 43 | * @return int 44 | */ 45 | public function get_user_id() { 46 | return $this->user->ID; 47 | } 48 | 49 | /** 50 | * Get the user that the token represents. 51 | * 52 | * @return WP_User 53 | */ 54 | public function get_user() { 55 | return $this->user; 56 | } 57 | 58 | /** 59 | * Get the meta prefix. 60 | * 61 | * @return string Meta prefix. 62 | */ 63 | abstract protected function get_meta_prefix(); 64 | 65 | /** 66 | * Check if the token is valid. 67 | * 68 | * @return bool True if the token is valid, false otherwise. 69 | */ 70 | abstract public function is_valid(); 71 | 72 | /** 73 | * Get the token's key. 74 | * 75 | * @return string Token 76 | */ 77 | public function get_key() { 78 | return $this->key; 79 | } 80 | 81 | /** 82 | * Get the token's value. 83 | * 84 | * @return mixed Token value, specific to the token type. 85 | */ 86 | public function get_value() { 87 | return $this->value; 88 | } 89 | 90 | /** 91 | * Get the meta key for the token. 92 | * 93 | * @return string Meta key, including type-specific prefix. 94 | */ 95 | public function get_meta_key() { 96 | return $this->get_meta_prefix() . $this->get_key(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /inc/types/class-implicit.php: -------------------------------------------------------------------------------- 1 | issue_token( $user ); 42 | if ( is_wp_error( $token ) ) { 43 | return $token; 44 | } 45 | 46 | $redirect_args = [ 47 | 'access_token' => $token->get_key(), 48 | 'token_type' => 'bearer', 49 | ]; 50 | break; 51 | 52 | case 'cancel': 53 | $redirect_args = [ 54 | 'error' => 'access_denied', 55 | ]; 56 | break; 57 | 58 | default: 59 | return new WP_Error( 60 | 'oauth2.types.authorization_code.handle_authorisation.invalid_action', 61 | __( 'Invalid form action.', 'oauth2' ) 62 | ); 63 | } 64 | 65 | if ( ! empty( $data['state'] ) ) { 66 | $redirect_args['state'] = $data['state']; 67 | } 68 | 69 | $redirect_args = $this->filter_redirect_args( 70 | $redirect_args, 71 | 'authorize' === $submit, 72 | $client, 73 | $data 74 | ); 75 | 76 | $fragment = build_query( $redirect_args ); 77 | $generated_redirect = $redirect_uri . '#' . $fragment; 78 | wp_safe_redirect( $generated_redirect ); 79 | exit; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /inc/types/class-authorization-code.php: -------------------------------------------------------------------------------- 1 | generate_authorization_code( $user ); 42 | if ( is_wp_error( $code ) ) { 43 | return $code; 44 | } 45 | 46 | $redirect_args = [ 47 | 'code' => $code->get_code(), 48 | ]; 49 | break; 50 | 51 | case 'cancel': 52 | $redirect_args = [ 53 | 'error' => 'access_denied', 54 | ]; 55 | break; 56 | 57 | default: 58 | return new WP_Error( 59 | 'oauth2.types.authorization_code.handle_authorisation.invalid_action', 60 | __( 'Invalid form action.', 'oauth2' ) 61 | ); 62 | } 63 | 64 | if ( ! empty( $data['state'] ) ) { 65 | $redirect_args['state'] = $data['state']; 66 | } 67 | 68 | $redirect_args = $this->filter_redirect_args( 69 | $redirect_args, 70 | 'authorize' === $submit, 71 | $client, 72 | $data 73 | ); 74 | 75 | $generated_redirect = add_query_arg( urlencode_deep( $redirect_args ), $redirect_uri ); 76 | // phpcs:ignore WordPress.Security.SafeRedirect -- Intentionally external redirect, secured via client registration. 77 | wp_redirect( $generated_redirect ); 78 | exit; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /plugin.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2019 Squiz Pty Ltd (ABN 77 084 670 600) 9 | * @license GPL-2.0-or-later 10 | * 11 | * @oauth2 12 | * Plugin Name: OAuth 2 for WordPress 13 | * Plugin URI: https://github.com/WP-API/OAuth2 14 | * Description: Connect apps to your site using OAuth 2. 15 | * Version: 0.2.0 16 | * Author: WordPress Core Contributors (REST API Focus) 17 | * Author URI: https://make.wordpress.org/core/ 18 | * License: GPL v2 or later 19 | * License URI: https://www.gnu.org/licenses/gpl-2.0.html 20 | * Text Domain: oauth2 21 | * Domain Path: /languages 22 | * Requires at least: 4.8 23 | * Requires PHP: 5.6 24 | */ 25 | 26 | namespace WP\OAuth2; 27 | 28 | // Avoid loading twice if loaded via App Connect. 29 | if ( class_exists( 'WP\\OAuth2\\Client' ) ) { 30 | return; 31 | } 32 | 33 | require __DIR__ . '/inc/namespace.php'; 34 | require __DIR__ . '/inc/class-clientinterface.php'; 35 | require __DIR__ . '/inc/class-client.php'; 36 | require __DIR__ . '/inc/class-personalclient.php'; 37 | require __DIR__ . '/inc/class-scopes.php'; 38 | require __DIR__ . '/inc/authentication/namespace.php'; 39 | require __DIR__ . '/inc/endpoints/namespace.php'; 40 | require __DIR__ . '/inc/endpoints/class-authorization.php'; 41 | require __DIR__ . '/inc/endpoints/class-token.php'; 42 | require __DIR__ . '/inc/tokens/namespace.php'; 43 | require __DIR__ . '/inc/tokens/class-token.php'; 44 | require __DIR__ . '/inc/tokens/class-access-token.php'; 45 | require __DIR__ . '/inc/tokens/class-authorization-code.php'; 46 | require __DIR__ . '/inc/types/class-type.php'; 47 | require __DIR__ . '/inc/types/class-base.php'; 48 | require __DIR__ . '/inc/types/class-authorization-code.php'; 49 | require __DIR__ . '/inc/types/class-implicit.php'; 50 | require __DIR__ . '/inc/admin/namespace.php'; 51 | require __DIR__ . '/inc/admin/profile/namespace.php'; 52 | require __DIR__ . '/inc/admin/profile/personaltokens/namespace.php'; 53 | 54 | bootstrap(); 55 | -------------------------------------------------------------------------------- /docs/advanced/Desktop.md: -------------------------------------------------------------------------------- 1 | # Desktop/Mobile Clients 2 | 3 | OAuth was originally designed for web applications, so desktop and mobile clients may wish to use the OAuth flow slightly differently. The authorization flow for OAuth requires both a browser and a callback URL for the second leg. 4 | 5 | 6 | ## Callback URL Schemes 7 | 8 | The OAuth plugin supports any valid URL scheme, including custom schemes. This allows using custom schemes for callback URLs, which can then trigger your application. Note that for custom schemes, the authority (user and password) part must be empty, and the host **must not be empty** and must not contain invalid characters (such as `:#?[]`). 9 | 10 | For example, the following URLs are **invalid**: 11 | * `custom-app://` 12 | * `custom-app://?oauth_callback` 13 | * `custom-app://user:pass@oauth_callback` 14 | 15 | The following URLs are **valid**: 16 | * `custom-app://oauth_callback` 17 | * `custom-app://oauth?callback` 18 | * `custom-app://oauth/callback` 19 | * `custom-app://oauth_callback:42` 20 | 21 | 22 | ## Out-of-Band Flow 23 | 24 | For clients without the ability to handle a callback URL, an out-of-band flow can be used. Rather than redirecting the user after authorization, this flow displays the verifier token to the user to copy to the client. 25 | 26 | To trigger the out-of-band flow, the callback URL must be set to `oob`. After the user has authorized the application, they'll be redirected to an internal page on the site which displays the verifier token. This can either be copy-and-pasted into the client (e.g. for command-line applications), or typed in manually. 27 | 28 | With the callback URL set to `oob`, the supplied callback and registered callback must match exactly, and must be `oob`. This means that clients can either have a callback URL **or** out-of-band handling, and cannot work with both. 29 | 30 | 31 | ## Best Practices 32 | 33 | * Clients should use callback URLs if at all possible, with out-of-band flow as a last resort. 34 | * Clients should prefer the system browser rather a built-in browser, as the former typically has allows better usage of saved passwords. Using a built-in browser also gives a dangerous signal to users, as a compromised app could fake a login screen and phish their credentials. 35 | -------------------------------------------------------------------------------- /docs/basics/Registering.md: -------------------------------------------------------------------------------- 1 | # Registering an Application 2 | 3 | Before you can talk to the server, you need to establish your credentials on the site. This involves registering your application with the site. 4 | 5 | Applications can only be registered by site administrators, and must be registered on each site individually. (We're working on making it possible in the future to register once with a central authority to make this process easier.) 6 | 7 | To register an application, open your site dashboard and head to Users > Applications, then click Add New. You'll need to enter the name of the application, an optional description, and the callback URL. This callback URL is used during the authorization process to redirect users back to after connecting. This URL can be changed later if you don't have a callback endpoint yet. 8 | 9 | ## Callback URLs 10 | 11 | The callback URL is used during the authorization process. After users authorize your application on the site, they'll be redirected back to your callback URL. This callback needs to save the verifier token passed in, which is used in the third leg of the flow. The callback also typically starts the third leg (token exchange) on the server side. (The next section, [the Authorization Flow](Auth-Flow.md), expands more on how the callback URL is used.) 12 | 13 | At the start of the OAuth flow, you pass in your OAuth callback URL for the specific request, which allows you to customise the callback URL for each request as needed. The OAuth plugin requires that your supplied callback URL match the scheme, authority (user and password part), host, port, and path of the registered callback URL. Only the query parameters and fragment (hash part) may differ from your registered URL. (This differs from some OAuth implementations, which allow subpaths of the callback URL.) 14 | 15 | For sites with multiple domains or subdomains (e.g. a WordPress multisite network), the recommended method for handling this is to have a singular "main" callback URL which redirects to the specific site. During the request process, the site ID can then be added to the callback URL as a query parameter. 16 | 17 | [Non-web applications](../advanced/Desktop.md) may wish to use custom URL schemes, or out-of-band handling. Out-of-band handling is triggered by setting the callback URL to the string `oob`. Rather than redirect after authorization, the site will instead display the verifier code to the user, which they then copy-and-paste or otherwise provide to the application. 18 | -------------------------------------------------------------------------------- /docs/advanced/Web.md: -------------------------------------------------------------------------------- 1 | # Web Clients 2 | 3 | OAuth was originally designed for working with web clients, so the process should be fairly smooth for most developers. There are some use cases that are tricky however, although not impossible to work with. 4 | 5 | 6 | ## Distributed or Multi-Domain Clients 7 | 8 | One issue for clients with multiple domains (such as multisite WordPress installs) is callbacks: clients can only have a single registered callback, with no variation in most of the URL. This can make working with multiple domains tricky. 9 | 10 | This can be easily handled by adding extra query parameters to the callback URL, as these **can** be set on a per-request basis. These can then be handled by your callback URL to pass on to a secondary callback. 11 | 12 | For example, for a WordPress multisite, set the callback URL to a URL on the main site. A `site={id}` parameter can then be added when setting the callback for the request. The callback can then redirect the user's browser to a per-site callback based on this parameter (ensuring to pass along the `oauth_token` and `oauth_verifier` parameters.) 13 | 14 | **Note:** When using this method, be sure to verify the site. Check that the request token being handled was actually created by the site asking for it. If you're using domains instead of site IDs, be *very* careful not to redirect to an unknown domain. Failure to check this can easily lead to CSRF (phishing) attacks. 15 | 16 | 17 | ## In-Browser Clients 18 | 19 | Increasingly with modern JavaScript-based applications, the application may run entirely in the user's browser. OAuth 1 was (unfortunately) not designed for this use case. OAuth 2 goes a long way to correcting this, but as mentioned previously, [we can't use it :(](../introduction/OAuth-1.md) 20 | 21 | This primarily falls down to the application secret. OAuth 1.0a relies on the client secret being secret (duh) as the basis for the authorization flow. This is core to the signature process. Without this being secret, other applications can issue their own tokens as your application. OAuth 2 makes allowances for clients with public secrets with the `implicit` flow. 22 | 23 | The simplest way to handle this is to introduce a minimal server-side component. This can be created from scratch, or a prebuilt server such as [Guardian](http://guardianjs.com/) can be used instead. 24 | 25 | 26 | ## Best Practices 27 | 28 | * Never expose secrets, such as in JS client-side applications. Instead, use a proxy to handle the OAuth authentication. 29 | * Similarly, never expose the verifier to sites outside of your control. The verifier is specifically intended for CSRF mitigation, so be exceedingly careful before passing it on to another URL. -------------------------------------------------------------------------------- /inc/class-clientinterface.php: -------------------------------------------------------------------------------- 1 | 'POST', 25 | 'callback' => [ $this, 'exchange_token' ], 26 | 'args' => [ 27 | 'grant_type' => [ 28 | 'required' => true, 29 | 'type' => 'string', 30 | 'validate_callback' => [ $this, 'validate_grant_type' ], 31 | ], 32 | 'client_id' => [ 33 | 'required' => true, 34 | 'type' => 'string', 35 | 'validate_callback' => 'rest_validate_request_arg', 36 | ], 37 | 'code' => [ 38 | 'required' => true, 39 | 'type' => 'string', 40 | 'validate_callback' => 'rest_validate_request_arg', 41 | ], 42 | ], 43 | ] 44 | ); 45 | } 46 | 47 | /** 48 | * Validates the given grant type. 49 | * 50 | * @param string $type Grant type. 51 | * 52 | * @return bool Whether or not the grant type is valid. 53 | */ 54 | public function validate_grant_type( $type ) { 55 | return 'authorization_code' === $type; 56 | } 57 | 58 | /** 59 | * Validates the token given in the request, and issues a new token for the user. 60 | * 61 | * @param WP_REST_Request $request Request object. 62 | * 63 | * @return array|WP_Error Token data on success, or error on failure. 64 | */ 65 | public function exchange_token( WP_REST_Request $request ) { 66 | $client = OAuth2\get_client( $request['client_id'] ); 67 | if ( empty( $client ) ) { 68 | return new WP_Error( 69 | 'oauth2.endpoints.token.exchange_token.invalid_client', 70 | /* translators: %s: client ID */ 71 | sprintf( __( 'Client ID %s is invalid.', 'oauth2' ), $request['client_id'] ), 72 | [ 73 | 'status' => WP_Http::BAD_REQUEST, 74 | 'client_id' => $request['client_id'], 75 | ] 76 | ); 77 | } 78 | 79 | $auth_code = $client->get_authorization_code( $request['code'] ); 80 | if ( is_wp_error( $auth_code ) ) { 81 | return $auth_code; 82 | } 83 | 84 | $is_valid = $auth_code->validate(); 85 | if ( is_wp_error( $is_valid ) ) { 86 | // Invalid request, but code itself exists, so we should delete 87 | // (and silently ignore errors). 88 | $auth_code->delete(); 89 | 90 | return $is_valid; 91 | } 92 | 93 | // Looks valid, delete the code and issue a token. 94 | $user = $auth_code->get_user(); 95 | if ( is_wp_error( $user ) ) { 96 | return $user; 97 | } 98 | 99 | $did_delete = $auth_code->delete(); 100 | if ( is_wp_error( $did_delete ) ) { 101 | return $did_delete; 102 | } 103 | 104 | $token = $client->issue_token( $user ); 105 | if ( is_wp_error( $token ) ) { 106 | return $token; 107 | } 108 | 109 | $data = [ 110 | 'access_token' => $token->get_key(), 111 | 'token_type' => 'bearer', 112 | ]; 113 | return $data; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /theme/oauth2-authorize.php: -------------------------------------------------------------------------------- 1 | 21 | 22 | 62 | 63 |
64 | 65 | %s', 68 | esc_html( 69 | sprintf( 70 | /* translators: %1$s: client name */ 71 | __( 'Connect %1$s', 'oauth2' ), 72 | $client->get_name() 73 | ) 74 | ) 75 | ); 76 | ?> 77 | 78 |
79 | 80 | ID, '78' ); ?> 81 | 82 | ' . __( 'Howdy %1$s,
"%2$s" would like to connect to %3$s.', 'oauth2' ) . '

', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 86 | esc_html( $_current_user->user_login ), 87 | esc_html( $client->get_name() ), 88 | esc_html( get_bloginfo( 'name' ) ) 89 | ); 90 | ?> 91 | 92 |
93 | 94 | tags. 97 | */ 98 | do_action( 'oauth2_authorize_form', $client ); 99 | wp_nonce_field( sprintf( 'oauth2_authorize:%s', $client->get_id() ) ); 100 | ?> 101 | 102 |

103 | 104 | 105 |

106 | 107 |
108 | 109 | 125 | 126 | get_pagenum(); 21 | 22 | $args = [ 23 | 'post_type' => Client::POST_TYPE, 24 | 'post_status' => 'any', 25 | 'paged' => $paged, 26 | ]; 27 | 28 | $query = new WP_Query( $args ); 29 | $this->items = $query->posts; 30 | 31 | $pagination_args = [ 32 | 'total_items' => $query->found_posts, 33 | 'total_pages' => $query->max_num_pages, 34 | 'per_page' => $query->get( 'posts_per_page' ), 35 | ]; 36 | $this->set_pagination_args( $pagination_args ); 37 | } 38 | 39 | /** 40 | * Get a list of columns for the list table. 41 | * 42 | * @since 3.1.0 43 | * @access public 44 | * 45 | * @return array Array in which the key is the ID of the column, 46 | * and the value is the description. 47 | */ 48 | public function get_columns() { 49 | $c = [ 50 | 'cb' => '', 51 | 'name' => __( 'Name', 'oauth2' ), 52 | 'description' => __( 'Description', 'oauth2' ), 53 | ]; 54 | 55 | return $c; 56 | } 57 | 58 | /** 59 | * @param \WP_Post $item Post object. 60 | */ 61 | public function column_cb( $item ) { 62 | ?> 63 | 65 | 66 | 68 | 69 | ID ); 78 | if ( empty( $title ) ) { 79 | $title = '' . esc_html__( 'Untitled', 'oauth2' ) . ''; 80 | } 81 | 82 | $edit_link = add_query_arg( 83 | [ 84 | 'page' => 'rest-oauth2-apps', 85 | 'action' => 'edit', 86 | 'id' => $item->ID, 87 | ], 88 | admin_url( 'users.php' ) 89 | ); 90 | $delete_link = add_query_arg( 91 | [ 92 | 'page' => 'rest-oauth2-apps', 93 | 'action' => 'delete', 94 | 'id' => $item->ID, 95 | ], 96 | admin_url( 'users.php' ) 97 | ); 98 | $delete_link = wp_nonce_url( $delete_link, 'rest-oauth2-delete:' . $item->ID ); 99 | 100 | $actions = [ 101 | 'edit' => sprintf( '%s', esc_url( $edit_link ), esc_html__( 'Edit', 'oauth2' ) ), 102 | 'delete' => sprintf( '%s', esc_url( $delete_link ), esc_html__( 'Delete', 'oauth2' ) ), 103 | ]; 104 | 105 | $post_type_object = get_post_type_object( $item->post_type ); 106 | if ( current_user_can( $post_type_object->cap->publish_posts ) && 'publish' !== $item->post_status ) { 107 | $publish_link = add_query_arg( 108 | [ 109 | 'page' => 'rest-oauth2-apps', 110 | 'action' => 'approve', 111 | 'id' => $item->ID, 112 | ], 113 | admin_url( 'users.php' ) 114 | ); 115 | 116 | $publish_link = wp_nonce_url( $publish_link, 'rest-oauth2-approve:' . $item->ID ); 117 | $actions['app-approve'] = sprintf( 118 | '%s', 119 | esc_url( $publish_link ), 120 | esc_html__( 'Approve', 'oauth2' ) 121 | ); 122 | } 123 | 124 | $action_html = $this->row_actions( $actions ); 125 | 126 | // Get suffixes for draft, etc 127 | ob_start(); 128 | _post_states( $item ); 129 | $title = sprintf( 130 | '%s%s', 131 | $edit_link, 132 | $title, 133 | ob_get_clean() 134 | ); 135 | 136 | return $title . ' ' . $action_html; 137 | } 138 | 139 | /** 140 | * @param \WP_Post $item Post object. 141 | * @return string Content of the column. 142 | */ 143 | protected function column_description( $item ) { 144 | return $item->post_content; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/install-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | 15 | WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} 16 | WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} 17 | 18 | download() { 19 | if [ `which curl` ]; then 20 | curl -s "$1" > "$2"; 21 | elif [ `which wget` ]; then 22 | wget -nv -O "$2" "$1" 23 | fi 24 | } 25 | 26 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then 27 | WP_TESTS_TAG="tags/$WP_VERSION" 28 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 29 | WP_TESTS_TAG="trunk" 30 | else 31 | # http serves a single offer, whereas https serves multiple. we only want one 32 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 33 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 34 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 35 | if [[ -z "$LATEST_VERSION" ]]; then 36 | echo "Latest WordPress version could not be found" 37 | exit 1 38 | fi 39 | WP_TESTS_TAG="tags/$LATEST_VERSION" 40 | fi 41 | 42 | set -ex 43 | 44 | install_wp() { 45 | 46 | if [ -d $WP_CORE_DIR ]; then 47 | return; 48 | fi 49 | 50 | mkdir -p $WP_CORE_DIR 51 | 52 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 53 | mkdir -p /tmp/wordpress-nightly 54 | download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip 55 | unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/ 56 | mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR 57 | else 58 | if [ $WP_VERSION == 'latest' ]; then 59 | local ARCHIVE_NAME='latest' 60 | else 61 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 62 | fi 63 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz 64 | tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR 65 | fi 66 | 67 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 68 | } 69 | 70 | install_test_suite() { 71 | # portable in-place argument for both GNU sed and Mac OSX sed 72 | if [[ $(uname -s) == 'Darwin' ]]; then 73 | local ioption='-i .bak' 74 | else 75 | local ioption='-i' 76 | fi 77 | 78 | # set up testing suite if it doesn't yet exist 79 | if [ ! -d $WP_TESTS_DIR ]; then 80 | # set up testing suite 81 | mkdir -p $WP_TESTS_DIR 82 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 83 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 84 | fi 85 | 86 | if [ ! -f wp-tests-config.php ]; then 87 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 88 | # remove all forward slashes in the end 89 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 90 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 91 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 92 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 93 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 94 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 95 | fi 96 | 97 | } 98 | 99 | install_db() { 100 | 101 | if [ ${SKIP_DB_CREATE} = "true" ]; then 102 | return 0 103 | fi 104 | 105 | # parse DB_HOST for port or socket references 106 | local PARTS=(${DB_HOST//\:/ }) 107 | local DB_HOSTNAME=${PARTS[0]}; 108 | local DB_SOCK_OR_PORT=${PARTS[1]}; 109 | local EXTRA="" 110 | 111 | if ! [ -z $DB_HOSTNAME ] ; then 112 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 113 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 114 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 115 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 116 | elif ! [ -z $DB_HOSTNAME ] ; then 117 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 118 | fi 119 | fi 120 | 121 | # create database 122 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 123 | } 124 | 125 | install_wp 126 | install_test_suite 127 | install_db 128 | -------------------------------------------------------------------------------- /inc/namespace.php: -------------------------------------------------------------------------------- 1 | register_hooks(); 41 | } 42 | 43 | /** 44 | * Get valid grant types. 45 | * 46 | * @return Type[] Map of grant type to handler object. 47 | */ 48 | function get_grant_types() { 49 | 50 | /** 51 | * Filter valid grant types. 52 | * 53 | * Default supported grant types are added in register_grant_types(). 54 | * Note that additional grant types must follow the extension policy in the 55 | * OAuth 2 specification. 56 | * 57 | * @param Type[] $grant_types Map of grant type to handler object. 58 | */ 59 | $grant_types = apply_filters( 'oauth2.grant_types', [] ); 60 | 61 | foreach ( $grant_types as $type => $handler ) { 62 | if ( ! $handler instanceof Type ) { 63 | /* translators: 1: Grant type name, 2: Grant type interface */ 64 | $message = esc_html__( 'Skipping invalid grant type "%1$s". Required interface "%1$s" not implemented.', 'oauth2' ); 65 | _doing_it_wrong( __FUNCTION__, sprintf( esc_html( $message ), esc_html( $type ), 'WP\\OAuth2\\Types\\Type' ), '0.1.0' ); 66 | unset( $grant_types[ $type ] ); 67 | } 68 | } 69 | 70 | return $grant_types; 71 | } 72 | 73 | /** 74 | * Register default grant types. 75 | * 76 | * Callback for the oauth2.grant_types hook. 77 | * 78 | * @param array $types Existing grant types. 79 | * 80 | * @return array Grant types with additional types registered. 81 | */ 82 | function register_grant_types( $types ) { 83 | $types['authorization_code'] = new Types\Authorization_Code(); 84 | $types['implicit'] = new Types\Implicit(); 85 | 86 | return $types; 87 | } 88 | 89 | /** 90 | * Register the OAuth 2 authentication scheme in the API index. 91 | * 92 | * @param WP_REST_Response $response Index response object. 93 | * 94 | * @return WP_REST_Response Update index repsonse object. 95 | */ 96 | function register_in_index( WP_REST_Response $response ) { 97 | $data = $response->get_data(); 98 | 99 | $data['authentication']['oauth2'] = [ 100 | 'endpoints' => [ 101 | 'authorization' => get_authorization_url(), 102 | 'token' => get_token_url(), 103 | ], 104 | 'grant_types' => array_keys( get_grant_types() ), 105 | ]; 106 | 107 | $response->set_data( $data ); 108 | 109 | return $response; 110 | } 111 | 112 | /** 113 | * Get the authorization endpoint URL. 114 | * 115 | * @return string URL for the OAuth 2 authorization endpoint. 116 | */ 117 | function get_authorization_url() { 118 | $url = wp_login_url(); 119 | $url = add_query_arg( 'action', 'oauth2_authorize', $url ); 120 | 121 | /** 122 | * Filter the authorization URL. 123 | * 124 | * @param string $url URL for the OAuth 2 authorization endpoint. 125 | */ 126 | return apply_filters( 'oauth2.get_authorization_url', $url ); 127 | } 128 | 129 | /** 130 | * Get the token endpoint URL. 131 | * 132 | * @return string URL for the OAuth 2 token endpoint. 133 | */ 134 | function get_token_url() { 135 | $url = rest_url( 'oauth2/access_token' ); 136 | 137 | /** 138 | * Filter the token URL. 139 | * 140 | * @param string $url URL for the OAuth 2 token endpoint. 141 | */ 142 | return apply_filters( 'oauth2.get_token_url', $url ); 143 | } 144 | 145 | /** 146 | * Get a client by ID. 147 | * 148 | * @param string $id ID for the client. 149 | * 150 | * @return ClientInterface Client instance. 151 | */ 152 | function get_client( $id ) { 153 | if ( PersonalClient::ID === $id ) { 154 | return PersonalClient::get_instance(); 155 | } 156 | 157 | return Client::get_by_id( $id ); 158 | } 159 | -------------------------------------------------------------------------------- /inc/class-personalclient.php: -------------------------------------------------------------------------------- 1 | $value ) { 34 | if ( strtolower( $key ) === 'authorization' ) { 35 | return $value; 36 | } 37 | } 38 | } 39 | 40 | return null; 41 | } 42 | 43 | /** 44 | * Extracts the token from the authorization header or the current request. 45 | * 46 | * @return string|null Token on success, null on failure. 47 | */ 48 | function get_provided_token() { 49 | $header = get_authorization_header(); 50 | if ( $header ) { 51 | return get_token_from_bearer_header( $header ); 52 | } 53 | 54 | $token = get_token_from_request(); 55 | if ( $token ) { 56 | return $token; 57 | } 58 | 59 | return null; 60 | } 61 | 62 | /** 63 | * Extracts the token from the given authorization header. 64 | * 65 | * @param string $header Authorization header. 66 | * 67 | * @return string|null Token on succes, null on failure. 68 | */ 69 | function get_token_from_bearer_header( $header ) { 70 | if ( is_string( $header ) && preg_match( '/Bearer ([a-zA-Z0-9\-._~\+\/=]+)/', trim( $header ), $matches ) ) { 71 | return $matches[1]; 72 | } 73 | 74 | return null; 75 | } 76 | 77 | /** 78 | * Extracts the token from the current request. 79 | * 80 | * @return string|null Token on succes, null on failure. 81 | */ 82 | function get_token_from_request() { 83 | if ( empty( $_GET['access_token'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended 84 | return null; 85 | } 86 | 87 | $token = $_GET['access_token']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput 88 | if ( is_string( $token ) ) { 89 | return $token; 90 | } 91 | 92 | // Got a token, but it's not valid. 93 | global $oauth2_error; 94 | $oauth2_error = create_invalid_token_error( $token ); 95 | return null; 96 | } 97 | 98 | /** 99 | * Try to authenticate if possible. 100 | * 101 | * @param WP_User|null $user Existing authenticated user. 102 | * 103 | * @return WP_User|int|WP_Error 104 | */ 105 | function attempt_authentication( $user = null ) { 106 | // Lock against infinite loops when querying the token itself. 107 | static $is_querying_token = false; 108 | global $oauth2_error; 109 | $oauth2_error = null; 110 | 111 | if ( ! empty( $user ) || $is_querying_token ) { 112 | return $user; 113 | } 114 | 115 | // Were we given a token? 116 | $token_value = get_provided_token(); 117 | if ( empty( $token_value ) ) { 118 | // No data provided, pass. 119 | return $user; 120 | } 121 | 122 | // Attempt to find the token. 123 | $is_querying_token = true; 124 | $token = Tokens\get_by_id( $token_value ); 125 | if ( empty( $token ) ) { 126 | $is_querying_token = false; 127 | $oauth2_error = create_invalid_token_error( $token_value ); 128 | return $user; 129 | } 130 | 131 | $client = $token->get_client(); 132 | $is_querying_token = false; 133 | 134 | if ( empty( $token ) || empty( $client ) ) { 135 | $oauth2_error = create_invalid_token_error( $token_value ); 136 | return $user; 137 | } 138 | 139 | // Token found, authenticate as the user. 140 | return $token->get_user_id(); 141 | } 142 | 143 | /** 144 | * Report our errors, if we have any. 145 | * 146 | * Attached to the rest_authentication_errors filter. Passes through existing 147 | * errors registered on the filter. 148 | * 149 | * @param WP_Error|null Current error, or null. 150 | * 151 | * @return WP_Error|null Error if one is set, otherwise null. 152 | */ 153 | function maybe_report_errors( $error = null ) { 154 | if ( ! empty( $error ) ) { 155 | return $error; 156 | } 157 | 158 | global $oauth2_error; 159 | return $oauth2_error; 160 | } 161 | 162 | /** 163 | * Creates an error object for the given invalid token. 164 | * 165 | * @param mixed $token Invalid token. 166 | * 167 | * @return WP_Error 168 | */ 169 | function create_invalid_token_error( $token ) { 170 | return new WP_Error( 171 | 'oauth2.authentication.attempt_authentication.invalid_token', 172 | __( 'Supplied token is invalid.', 'oauth2' ), 173 | [ 174 | 'status' => \WP_Http::FORBIDDEN, 175 | 'token' => $token, 176 | ] 177 | ); 178 | } 179 | -------------------------------------------------------------------------------- /docs/basics/Auth-Flow.md: -------------------------------------------------------------------------------- 1 | # The Authorization Flow 2 | 3 | The key to understanding how OAuth works is understanding the authorization flow. This is the process clients go through to link to a site. 4 | 5 | The flow with the OAuth plugin is called the **three-legged flow**, thanks to the three primary steps involved: 6 | 7 | * **Temporary Credentials Acquisition**: The client gets a set of temporary credentials from the server. 8 | * **Authorization**: The user "authorizes" the request token to access their account. 9 | * **Token Exchange**: The client exchanges the short-lived temporary credentials for a long-lived token. 10 | 11 | ## Temporary Credentials Acquisition 12 | 13 | The first step to authorization is acquiring temporary credentials (also known as a **Request Token**). These credentials are short-lived (typically 24 hours), and are used purely for the initial authorization process. They don't grant any access to data on the server, and cannot be used for anything except the authorization flow. 14 | 15 | These credentials are acquired by an initial HTTP request to the server. The client starts by sending a POST request to the temporary credential URL, typically `/oauth1/request` with the plugin. (This URL should be autodiscovered from the API, as individual sites may move this route, or delegate the process to another server.) This looks something like: 16 | 17 | This request includes the client key (`oauth_consumer_key`), the authorization callback (`oauth_callback`), and the request signature (`oauth_signature` and `oauth_signature_method`). This looks something like: 18 | 19 | ``` 20 | POST /oauth1/request HTTP/1.1 21 | Host: server.example.com 22 | Authorization: OAuth realm="Example", 23 | oauth_consumer_key="jd83jd92dhsh93js", 24 | oauth_signature_method="HMAC-SHA1", 25 | oauth_timestamp="123456789", 26 | oauth_nonce="7d8f3e4a", 27 | oauth_callback="http%3A%2F%2Fclient.example.com%2Fcb", 28 | oauth_signature="..." 29 | ``` 30 | 31 | The server checks the key and signature to ensure the client is valid. It also checks the callback to ensure it's valid for the client. 32 | 33 | Once the checks are complete, the server creates a new set of Temporary Credentials (`oauth_token` and `oauth_token_secret`) and returns them in the HTTP response (URL encoded). This looks something like: 34 | 35 | ``` 36 | HTTP/1.1 200 OK 37 | Content-Type: application/x-www-form-urlencoded 38 | 39 | oauth_token=hdk48Djdsa&oauth_token_secret=xyz4992k83j47x0b&oauth_callback_confirmed=true 40 | ``` 41 | 42 | These credentials are then used as the `oauth_token` and `oauth_token_secret` parameters for the Authorization and Token Exchange steps. 43 | 44 | (The `oauth_callback_confirmed=true` will always be returned, and indicates that the protocol is OAuth 1.0a.) 45 | 46 | 47 | ## Authorization 48 | 49 | The next step in the flow is the authorization process. This is a user-facing step, and the one that most users will be familiar with. 50 | 51 | Using the authorization URL supplied by the site (typically `/oauth1/authorize`), the client appends the temporary credential key (`oauth_token` from above) to the URL as a query parameter (again as `oauth_token`). The client then directs the user to this URL. Typically, this is done via a redirect for in-browser clients, or opening a browser for native clients. 52 | 53 | The user then logs in if they aren't already, and authorizes the client. They can also choose to cancel the authorization process if they don't want to link the client. 54 | 55 | If the user authorizes the client, the site then marks the token as authorized, and redirects the user back to the callback URL. The callback URL includes two extra query parameters: `oauth_token` (the same temporary credential token) and `oauth_verifier`, a CSRF token that needs to be passed in the next step. 56 | 57 | 58 | ## Token Exchange 59 | 60 | The final step in authorization is to exchange the temporary credentials (request token) for long-lived credentials (also known as an **Access Token**). This request also destroys the temporary credentials. 61 | 62 | The temporary credentials are converted to long-lived credentials by sending a POST request to the token request endpoint (typically `/oauth1/access`). This request must be signed by the temporary credentials, and must include the `oauth_verifier` token from the authorization step. The request looks something like: 63 | 64 | ``` 65 | POST /oauth1/access HTTP/1.1 66 | Host: server.example.com 67 | Authorization: OAuth realm="Example", 68 | oauth_consumer_key="jd83jd92dhsh93js", 69 | oauth_token="hdk48Djdsa", 70 | oauth_signature_method="HMAC-SHA1", 71 | oauth_timestamp="123456789", 72 | oauth_nonce="7d8f3e4a", 73 | oauth_verifier="473f82d3", 74 | oauth_signature="..." 75 | ``` 76 | 77 | The server again checks the key and signature, as well as also checking the verifier token to [avoid CSRF attacks](http://oauth.net/advisories/2009-1/). 78 | 79 | Assuming these checks all pass, the server will respond with the final set of credentials in the HTTP response body (form data, URL-encoded): 80 | 81 | ``` 82 | HTTP/1.1 200 OK 83 | Content-Type: application/x-www-form-urlencoded 84 | 85 | oauth_token=j49ddk933skd9dks&oauth_token_secret=ll399dj47dskfjdk 86 | ``` 87 | 88 | At this point, you can now discard the temporary credentials (as they are now useless), as well as the verifier token. 89 | 90 | Congratulations, your client is now linked to the site! -------------------------------------------------------------------------------- /inc/tokens/class-access-token.php: -------------------------------------------------------------------------------- 1 | value['client'] ); 34 | } 35 | 36 | /** 37 | * Get creation time for the token. 38 | * 39 | * @return int Creation timestamp. 40 | */ 41 | public function get_creation_time() { 42 | return $this->value['created']; 43 | } 44 | 45 | /** 46 | * Get a meta value for the token. 47 | * 48 | * This is used to store additional information on the token itself, such 49 | * as a description for the token. 50 | * 51 | * @param string $key Meta key to fetch. 52 | * @param mixed $default Value to return if key is unavailable. 53 | * 54 | * @return mixed Value if available, or value of `$default` if not found. 55 | */ 56 | public function get_meta( $key, $default = null ) { 57 | if ( empty( $this->value['meta'] ) || ! isset( $this->value['meta'][ $key ] ) ) { 58 | return null; 59 | } 60 | 61 | return $this->value['meta'][ $key ]; 62 | } 63 | 64 | /** 65 | * Set a meta value for the token. 66 | * 67 | * This is used to store additional information on the token itself, such 68 | * as a description for the token. 69 | * 70 | * @param string $key Meta key to set. 71 | * @param mixed $value Value to set on the key. 72 | * 73 | * @return bool True if meta was set, false otherwise. 74 | */ 75 | public function set_meta( $key, $value ) { 76 | if ( empty( $this->value['meta'] ) ) { 77 | $this->value['meta'] = []; 78 | } 79 | $this->value['meta'][ $key ] = $value; 80 | 81 | return update_user_meta( $this->get_user_id(), wp_slash( $this->get_meta_key() ), wp_slash( $this->value ) ); 82 | } 83 | 84 | /** 85 | * Revoke the token. 86 | * 87 | * @return bool|WP_Error True if succeeded, error otherwise. 88 | * @internal This may return other error codes in the future, as we may 89 | * need to also revoke refresh tokens. 90 | */ 91 | public function revoke() { 92 | $success = delete_user_meta( $this->get_user_id(), $this->get_meta_key() ); 93 | if ( ! $success ) { 94 | return new WP_Error( 95 | 'oauth2.tokens.access_token.revoke.could_not_revoke', 96 | __( 'Could not revoke the token.', 'oauth2' ) 97 | ); 98 | } 99 | 100 | return true; 101 | } 102 | 103 | /** 104 | * Get a token by ID. 105 | * 106 | * @param string $id Token ID. 107 | * 108 | * @return static|null Token if ID is found, null otherwise. 109 | */ 110 | public static function get_by_id( $id ) { 111 | $key = static::META_PREFIX . $id; 112 | $args = [ 113 | 'number' => 1, 114 | 'count_total' => false, 115 | 116 | // We use an EXISTS query here, limited by 1, so we can ignore 117 | // the performance warning. 118 | 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 119 | [ 120 | 'key' => $key, 121 | 'compare' => 'EXISTS', 122 | ], 123 | ], 124 | ]; 125 | 126 | $query = new WP_User_Query( $args ); 127 | $results = $query->get_results(); 128 | if ( empty( $results ) ) { 129 | return null; 130 | } 131 | 132 | $user = $results[0]; 133 | $value = get_user_meta( $user->ID, wp_slash( $key ), false ); 134 | if ( empty( $value ) ) { 135 | return null; 136 | } 137 | 138 | return new static( $user, $id, $value[0] ); 139 | } 140 | 141 | /** 142 | * Get all tokens for the specified user. 143 | * 144 | * @return static[] List of tokens. 145 | */ 146 | public static function get_for_user( WP_User $user ) { 147 | $meta = get_user_meta( $user->ID ); 148 | $tokens = []; 149 | foreach ( $meta as $key => $values ) { 150 | if ( strpos( $key, static::META_PREFIX ) !== 0 ) { 151 | continue; 152 | } 153 | 154 | $real_key = substr( $key, strlen( static::META_PREFIX ) ); 155 | $value = maybe_unserialize( $values[0] ); 156 | $tokens[] = new static( $user, $real_key, $value ); 157 | } 158 | 159 | return $tokens; 160 | } 161 | 162 | /** 163 | * Creates a new token for the given client and user. 164 | * 165 | * @param Client $client 166 | * @param WP_User $user 167 | * 168 | * @return Access_Token|WP_Error Token instance, or error on failure. 169 | */ 170 | public static function create( ClientInterface $client, WP_User $user, $meta = [] ) { 171 | if ( ! $user->exists() ) { 172 | return new WP_Error( 173 | 'oauth2.tokens.access_token.create.no_user', 174 | __( 'Invalid user to create token for.', 'oauth2' ) 175 | ); 176 | } 177 | 178 | $data = [ 179 | 'client' => $client->get_id(), 180 | 'created' => time(), 181 | 'meta' => $meta, 182 | ]; 183 | $key = wp_generate_password( static::KEY_LENGTH, false ); 184 | $meta_key = static::META_PREFIX . $key; 185 | 186 | $result = add_user_meta( $user->ID, wp_slash( $meta_key ), wp_slash( $data ), true ); 187 | if ( ! $result ) { 188 | return new WP_Error( 189 | 'oauth2.tokens.access_token.create.could_not_create', 190 | __( 'Unable to create token.', 'oauth2' ) 191 | ); 192 | } 193 | 194 | return new static( $user, $key, $data ); 195 | } 196 | 197 | /** 198 | * Check if the token is valid. 199 | * 200 | * @return bool True if the token is valid, false otherwise. 201 | */ 202 | public function is_valid() { 203 | return true; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /inc/tokens/class-authorization-code.php: -------------------------------------------------------------------------------- 1 | client = $client; 45 | $this->code = $code; 46 | } 47 | 48 | /** 49 | * Get the actual code. 50 | * 51 | * @return string Authorization code for passing to client. 52 | */ 53 | public function get_code() { 54 | return $this->code; 55 | } 56 | 57 | /** 58 | * Get meta key. 59 | * 60 | * Authorization codes are stored as post meta on the client. 61 | * 62 | * @return string 63 | */ 64 | protected function get_meta_key() { 65 | return static::KEY_PREFIX . $this->code; 66 | } 67 | 68 | /** 69 | * Get meta value. 70 | * 71 | * @return array|null Data if available, or null if code does not exist. 72 | */ 73 | protected function get_value() { 74 | $data = get_post_meta( $this->client->get_post_id(), wp_slash( $this->get_meta_key() ), false ); 75 | if ( empty( $data ) ) { 76 | return null; 77 | } 78 | 79 | return $data[0]; 80 | } 81 | 82 | /** 83 | * Get the user for the authorization code. 84 | * 85 | * @return WP_User|WP_Error User object, or error if data is not valid. 86 | */ 87 | public function get_user() { 88 | $value = $this->get_value(); 89 | if ( empty( $value ) || empty( $value['user'] ) ) { 90 | return new WP_Error( 91 | 'oauth2.tokens.authorization_code.get_user.invalid_data', 92 | __( 'Authorization code data is not valid.', 'oauth2' ) 93 | ); 94 | } 95 | 96 | return get_user_by( 'id', (int) $value['user'] ); 97 | } 98 | 99 | /** 100 | * Get the expiration. 101 | * 102 | * @return int|WP_Error Expiration, or error on failure. 103 | */ 104 | public function get_expiration() { 105 | $value = $this->get_value(); 106 | if ( empty( $value ) || empty( $value['expiration'] ) || ! is_numeric( $value['expiration'] ) ) { 107 | return new WP_Error( 108 | 'oauth2.tokens.authorization_code.get_user.invalid_data', 109 | __( 'Authorization code data is not valid.', 'oauth2' ) 110 | ); 111 | } 112 | 113 | return (int) $value['expiration']; 114 | } 115 | 116 | /** 117 | * Validate the code for use. 118 | * 119 | * @param array $args Other request arguments to validate. 120 | * @return bool|WP_Error True if valid, error describing problem otherwise. 121 | */ 122 | public function validate( $args = [] ) { 123 | $expiration = $this->get_expiration(); 124 | $now = time(); 125 | if ( $expiration <= $now ) { 126 | return new WP_Error( 127 | 'oauth2.tokens.authorization_code.validate.expired', 128 | __( 'Authorization code has expired.', 'oauth2' ), 129 | [ 130 | 'status' => WP_Http::BAD_REQUEST, 131 | 'expiration' => $expiration, 132 | 'time' => $now, 133 | ] 134 | ); 135 | } 136 | 137 | return true; 138 | } 139 | 140 | /** 141 | * Delete the authorization code. 142 | * 143 | * @return bool|WP_Error True if deleted, error otherwise. 144 | */ 145 | public function delete() { 146 | $result = delete_post_meta( $this->client->get_post_id(), wp_slash( $this->get_meta_key() ) ); 147 | if ( ! $result ) { 148 | return new WP_Error( 149 | 'oauth2.tokens.authorization_code.delete.could_not_delete', 150 | __( 'Unable to delete authorization code.', 'oauth2' ) 151 | ); 152 | } 153 | 154 | return true; 155 | } 156 | 157 | /** 158 | * Creates a new authorization code instance for the given client and code. 159 | * 160 | * @param Client $client 161 | * @param string $code 162 | * 163 | * @return Authorization_Code|WP_Error Authorization code instance, or error on failure. 164 | */ 165 | public static function get_by_code( Client $client, $code ) { 166 | $key = static::KEY_PREFIX . $code; 167 | $value = get_post_meta( $client->get_post_id(), wp_slash( $key ), false ); 168 | if ( empty( $value ) ) { 169 | return new WP_Error( 170 | 'oauth2.client.check_authorization_code.invalid_code', 171 | __( 'Authorization code is not valid for the specified client.', 'oauth2' ), 172 | [ 173 | 'status' => WP_Http::NOT_FOUND, 174 | 'client' => $client->get_id(), 175 | 'code' => $code, 176 | ] 177 | ); 178 | } 179 | 180 | return new static( $client, $code ); 181 | } 182 | 183 | /** 184 | * Creates a new authorization code instance for the given client and user. 185 | * 186 | * @param Client $client 187 | * @param WP_User $user 188 | * 189 | * @return Authorization_Code|WP_Error Authorization code instance, or error on failure. 190 | */ 191 | public static function create( Client $client, WP_User $user ) { 192 | $code = wp_generate_password( static::KEY_LENGTH, false ); 193 | $meta_key = static::KEY_PREFIX . $code; 194 | $data = [ 195 | 'user' => (int) $user->ID, 196 | 'expiration' => time() + static::MAX_AGE, 197 | ]; 198 | $result = add_post_meta( $client->get_post_id(), wp_slash( $meta_key ), wp_slash( $data ), true ); 199 | if ( ! $result ) { 200 | return new WP_Error( 201 | 'oauth2.tokens.authorization_code.create.could_not_create', 202 | __( 'Unable to create authorization code.', 'oauth2' ) 203 | ); 204 | } 205 | 206 | return new static( $client, $code ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | 15 | TMPDIR=${TMPDIR-/tmp} 16 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 17 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 18 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} 19 | 20 | download() { 21 | if [ `which curl` ]; then 22 | curl -s "$1" > "$2"; 23 | elif [ `which wget` ]; then 24 | wget -nv -O "$2" "$1" 25 | fi 26 | } 27 | 28 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then 29 | WP_BRANCH=${WP_VERSION%\-*} 30 | WP_TESTS_TAG="branches/$WP_BRANCH" 31 | 32 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 33 | WP_TESTS_TAG="branches/$WP_VERSION" 34 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 35 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 36 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 37 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 38 | else 39 | WP_TESTS_TAG="tags/$WP_VERSION" 40 | fi 41 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 42 | WP_TESTS_TAG="trunk" 43 | else 44 | # http serves a single offer, whereas https serves multiple. we only want one 45 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 46 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 47 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 48 | if [[ -z "$LATEST_VERSION" ]]; then 49 | echo "Latest WordPress version could not be found" 50 | exit 1 51 | fi 52 | WP_TESTS_TAG="tags/$LATEST_VERSION" 53 | fi 54 | set -ex 55 | 56 | install_wp() { 57 | 58 | if [ -d $WP_CORE_DIR ]; then 59 | return; 60 | fi 61 | 62 | mkdir -p $WP_CORE_DIR 63 | 64 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 65 | mkdir -p $TMPDIR/wordpress-nightly 66 | download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip 67 | unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ 68 | mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR 69 | else 70 | if [ $WP_VERSION == 'latest' ]; then 71 | local ARCHIVE_NAME='latest' 72 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 73 | # https serves multiple offers, whereas http serves single. 74 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 75 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 76 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 77 | LATEST_VERSION=${WP_VERSION%??} 78 | else 79 | # otherwise, scan the releases and get the most up to date minor version of the major release 80 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 81 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 82 | fi 83 | if [[ -z "$LATEST_VERSION" ]]; then 84 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 85 | else 86 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 87 | fi 88 | else 89 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 90 | fi 91 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 92 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 93 | fi 94 | 95 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 96 | } 97 | 98 | install_test_suite() { 99 | # portable in-place argument for both GNU sed and Mac OSX sed 100 | if [[ $(uname -s) == 'Darwin' ]]; then 101 | local ioption='-i.bak' 102 | else 103 | local ioption='-i' 104 | fi 105 | 106 | # set up testing suite if it doesn't yet exist 107 | if [ ! -d $WP_TESTS_DIR ]; then 108 | # set up testing suite 109 | mkdir -p $WP_TESTS_DIR 110 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 111 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 112 | fi 113 | 114 | if [ ! -f wp-tests-config.php ]; then 115 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 116 | # remove all forward slashes in the end 117 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 118 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 119 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 120 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 121 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 122 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 123 | fi 124 | 125 | } 126 | 127 | install_db() { 128 | 129 | if [ ${SKIP_DB_CREATE} = "true" ]; then 130 | return 0 131 | fi 132 | 133 | # parse DB_HOST for port or socket references 134 | local PARTS=(${DB_HOST//\:/ }) 135 | local DB_HOSTNAME=${PARTS[0]}; 136 | local DB_SOCK_OR_PORT=${PARTS[1]}; 137 | local EXTRA="" 138 | 139 | if ! [ -z $DB_HOSTNAME ] ; then 140 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 141 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 142 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 143 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 144 | elif ! [ -z $DB_HOSTNAME ] ; then 145 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 146 | fi 147 | fi 148 | 149 | # create database 150 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 151 | } 152 | 153 | install_wp 154 | install_test_suite 155 | install_db 156 | -------------------------------------------------------------------------------- /docs/basics/Signing.md: -------------------------------------------------------------------------------- 1 | # Signing Requests 2 | 3 | One of the hardest parts of working with OAuth 1 is signing requests. It's important to understand the process from the start. 4 | 5 | Even once you understand the process, it's recommended you use an existing library for this process. There are a lot of intricacies and edge cases to signing requests that are easy to miss. If you're ever in doubt on any details, the [OAuth RFC](https://tools.ietf.org/html/rfc5849#section-3.4) is the canonical reference on signatures; this is only an easier guide to it. 6 | 7 | Request signing in OAuth is a key part of ensuring your application can't be spoofed. This uses a pre-established **shared secret** only known by the server and the client, which is a key reason why you should keep your credentials secret. This secret is then mixed with the request data and a nonce to ensure the signature can't be used multiple times. 8 | 9 | **Note for experienced developers:** The OAuth plugin only supports HMAC-SHA1 signatures, and PHP-style GET parameters (`a[]=1&a[]=2`) are treated literally, with the `[]` included in the parameter names. **This may differ from other PHP-powered OAuth servers.** 10 | 11 | 12 | ## Base String 13 | 14 | Before you can create a signature, you need something to sign. The first step is to take the request you're about to send and turn it into a single string. This needs to take into consideration the whole request, so it's generate it as late as possible. Ideally, using an OAuth implementation built into your HTTP client will ensure your base string is accurate. 15 | 16 | The base string uses three pieces of data: the HTTP method (`GET`, `POST`, etc), the URL (without GET parameters), and any passed parameters. These follow [a very specific set of rules](https://tools.ietf.org/html/rfc5849#section-3.4.1), which loosely summarised are: 17 | 18 | * **Method:** Uppercase HTTP method. 19 | * **URL:** Lowercase scheme and host, port excluded if 80 for HTTP or 443 for SSL. 20 | * **Request Parameters**: OAuth parameters from `Authorization` header (excluding `oauth_signature` itself), GET parameters from the URL, and POST parameters if they're form encoded (`a=b&c=d` format; **not** JSON). Encode the name and value for each, sort by name (and value for duplicate keys). Combine key and value with a `=`, then concatenate with `&` into a string. 21 | 22 | These pieces are then combined by URL-encoding each, then concatenating with `&` into a single string. 23 | 24 | For example, for the following request: 25 | ``` 26 | POST /wp-json/wp/v2/posts 27 | Host: example.com 28 | Authorization: OAuth 29 | oauth_consumer_key="key" 30 | oauth_token="token" 31 | oauth_signature_method="HMAC-SHA1" 32 | oauth_timestamp="123456789", 33 | oauth_nonce="nonce", 34 | oauth_signature="..." 35 | 36 | { 37 | "title": "Hello World!" 38 | } 39 | ``` 40 | 41 | The base string pieces are: 42 | * Method: `POST` 43 | * URL: `http://example.com/wp-json/wp/v2/posts` 44 | * Params: `oauth_consumer_key=key&oauth_nonce=nonce&oauth_signature_method=HMAC-SHA1&oauth_timestamp=123456789&oauth_token=token` 45 | 46 | The resulting base string would then be: 47 | ``` 48 | POST&http%3A%2F%2Fexample.com%2Fwp-json%2Fwp%2Fv2%2Fposts&oauth_consumer_key%3Dkey%26oauth_nonce%3Dnonce%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D123456789%26oauth_token%3Dtoken 49 | ``` 50 | 51 | 52 | ## Signature Key 53 | 54 | The OAuth plugin only supports a single signature method: HMAC-SHA1. This uses a HMAC (Hash-based Message Authentication Code), which looks similar to a normal SHA1 hash, but differs significantly. Importantly, it's immune to [length extension attacks](https://en.wikipedia.org/wiki/Length_extension_attack). It also needs two pieces: a **key** and the **text** to hash. The text is the base string created above. 55 | 56 | The signature key for HMAC-SHA1 is created by taking the client/consumer secret and the token secret, URL-encoding each, then concatenating them with `&` into a string. 57 | 58 | This process is always the same, **even if you don't have a token yet**. 59 | 60 | For example, if your client secret is `abcd` and your token secret is `1234`, the key is `abcd&1234`. If your client secret is `abcd`, and you don't have a token yet, the key is `abcd&`. 61 | 62 | 63 | ## Signature 64 | 65 | Once you have the base string and the signature, you can create the signature itself. The OAuth plugin only supports HMAC-SHA1 signatures, so the signature is always set to the result of `HMAC-SHA (key, text)`. 66 | 67 | The HMAC key should be set to the signature key as above, and the HMAC text should be set to the base string. The result of the HMAC hashing is used as the signature. 68 | 69 | (The hash should be the base64-encoded digest. Many languages handle this by default, but you may need to base64-encode it manually if not. This should always look like "wOJIO9A2W5mFwDgiDvZbTSMK/PY=", not raw binary data.) 70 | 71 | Even if you're writing the signature handling from scratch, the HMAC hashing should **always be handled by an existing library**. HMAC-SHA1 is built into many languages natively, and libraries are available for basically every other language. Do not write your own code to handle hashing. 72 | 73 | For example, in PHP, the `hash_hmac` function can be used to generate HMAC hashes: 74 | 75 | ```php 76 | $base_string = 'POST&http...'; 77 | $key = 'abcd&1234'; 78 | 79 | $signature = hash_hmac( 'sha1', $base_string, $key ); 80 | ``` 81 | 82 | 83 | ## Common Problems 84 | 85 | Signatures are without a doubt the hardest part of the OAuth 1 process. If your signature is incorrect, you'll receive a `json_oauth1_signature_mismatch`. Here's a couple of things that are easy to fix. 86 | 87 | 88 | ### Array Parameters 89 | 90 | If you're generating your signature in PHP and you have array parameters (that is, `a[]=1&a[]=2`), you may be generating parameters incorrectly. Some PHP signature implementations incorrectly treat this as `a=1&a=2`, or may even generate `a=Array`. Check that your implementation correctly generates these. 91 | 92 | ### JSON Data 93 | 94 | When sending data to the REST API, you'll likely be sending JSON data as the body. These parameters should **not** be included in the base string; the OAuth specification explicitly states that only form-encoded data should be included. 95 | -------------------------------------------------------------------------------- /inc/admin/profile/personaltokens/namespace.php: -------------------------------------------------------------------------------- 1 | ID ); 52 | } 53 | 54 | if ( ! $user_id && IS_PROFILE_PAGE ) { 55 | $user_id = $current_user->ID; 56 | } 57 | 58 | $user = get_user_by( 'id', $user_id ); 59 | if ( empty( $user ) ) { 60 | wp_die( esc_html__( 'Invalid user ID.' ) ); 61 | } 62 | if ( ! current_user_can( 'edit_user', $user_id ) ) { 63 | wp_die( esc_html__( 'Sorry, you are not allowed to edit this user.' ) ); 64 | } 65 | 66 | if ( current_user_can( 'edit_users' ) && ! IS_PROFILE_PAGE ) { 67 | $submenu_file = 'users.php'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 68 | } else { 69 | $submenu_file = 'profile.php'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 70 | } 71 | 72 | if ( current_user_can( 'edit_users' ) ) { 73 | $parent_file = 'users.php'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 74 | } else { 75 | $parent_file = 'profile.php'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 76 | } 77 | } 78 | 79 | /** 80 | * Render the access token creation page. 81 | */ 82 | function render_page() { 83 | bootstrap_profile_page(); 84 | 85 | $user = get_user_by( 'id', $GLOBALS['user_id'] ); 86 | 87 | if ( isset( $_POST['oauth2_action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing 88 | $error = handle_page_action( $user ); 89 | 90 | if ( is_wp_error( $error ) ) { 91 | add_action( 92 | 'all_admin_notices', 93 | function () use ( $error ) { 94 | echo '

' . esc_html( $error->get_error_message() ) . '

'; 95 | } 96 | ); 97 | } 98 | } 99 | 100 | $GLOBALS['title'] = __( 'Personal Access Tokens', 'oauth2' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 101 | require ABSPATH . 'wp-admin/admin-header.php'; 102 | 103 | $tokens = Access_Token::get_for_user( $user ); 104 | $tokens = array_filter( 105 | $tokens, 106 | function ( Access_Token $token ) { 107 | $client = $token->get_client(); 108 | return ! empty( $client ) && $client instanceof PersonalClient; 109 | } 110 | ); 111 | ?> 112 |
113 |

114 | 115 |

116 | 117 |
118 | 119 | 120 | 123 | 134 | 135 |
121 | 122 | 124 | 131 | 132 |

133 |
136 | 137 | 138 | 139 | 140 | 141 |

142 | 143 |

144 |
145 |
146 | $name, 192 | ]; 193 | $token = $client->issue_token( $user, $meta ); 194 | 195 | render_create_success( $user, $token ); 196 | } 197 | 198 | /** 199 | * @param WP_User $user 200 | * @param $token 201 | */ 202 | function render_create_success( WP_User $user, $token ) { 203 | $GLOBALS['title'] = __( 'Personal Access Tokens', 'oauth2' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 204 | require ABSPATH . 'wp-admin/admin-header.php'; 205 | ?> 206 | 207 |
208 |

209 |

210 | 211 |
get_key() ); ?>
212 | 213 |

214 |
215 | 216 | WP_Http::BAD_REQUEST, 57 | 'client_id' => $client_id, 58 | ] 59 | ); 60 | } 61 | 62 | // Validate the redirection URI. 63 | $redirect_uri = $this->validate_redirect_uri( $client, $redirect_uri ); 64 | if ( is_wp_error( $redirect_uri ) ) { 65 | return $redirect_uri; 66 | } 67 | 68 | // Valid parameters, ensure the user is logged in. 69 | if ( ! is_user_logged_in() ) { 70 | $redirect = ''; 71 | if ( isset( $_SERVER['REQUEST_URI'] ) ) { 72 | $redirect = $_SERVER['REQUEST_URI']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput 73 | } 74 | $url = wp_login_url( $redirect ); 75 | wp_safe_redirect( $url ); 76 | exit; 77 | } 78 | 79 | if ( empty( $_POST['_wpnonce'] ) ) { 80 | return $this->render_form( $client ); 81 | } 82 | 83 | // Check nonce. 84 | $nonce_action = $this->get_nonce_action( $client ); 85 | if ( ! wp_verify_nonce( wp_unslash( $_POST['_wpnonce'] ), $nonce_action ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 86 | return new WP_Error( 87 | 'oauth2.types.authorization_code.handle_authorisation.invalid_nonce', 88 | __( 'Invalid nonce.', 'oauth2' ) 89 | ); 90 | } 91 | 92 | if ( empty( $_POST['wp-submit'] ) ) { 93 | // Submitted, but button not selected... 94 | $error = new WP_Error( 95 | 'oauth2.types.authorization_code.handle_authorisation.invalid_submit', 96 | sprintf( 97 | /* translators: %1$s is the translated "Authorize" button, %2$s is the translated "Cancel" button */ 98 | __( 'Select either %1$s or %2$s to continue.', 'oauth2' ), 99 | __( 'Authorize', 'oauth2' ), 100 | __( 'Cancel', 'oauth2' ) 101 | ) 102 | ); 103 | return $this->render_form( $client, $error ); 104 | } 105 | 106 | $submit = sanitize_text_field( wp_unslash( $_POST['wp-submit'] ) ); 107 | 108 | $data = compact( 'redirect_uri', 'scope', 'state' ); 109 | return $this->handle_authorization_submission( $submit, $client, $data ); 110 | } 111 | 112 | /** 113 | * Validate the supplied redirect URI. 114 | * 115 | * @param Client $client Client to validate against. 116 | * @param string|null $redirect_uri Redirect URI, if supplied. 117 | * @return string|WP_Error Valid redirect URI on success, error otherwise. 118 | */ 119 | protected function validate_redirect_uri( Client $client, $redirect_uri = null ) { 120 | if ( empty( $redirect_uri ) ) { 121 | $registered = $client->get_redirect_uris(); 122 | if ( count( $registered ) !== 1 ) { 123 | // Either none registered, or more than one, so error. 124 | return new WP_Error( 125 | 'oauth2.types.authorization_code.handle_authorisation.missing_redirect_uri', 126 | __( 'Redirect URI was required, but not found.', 'oauth2' ) 127 | ); 128 | } 129 | 130 | $redirect_uri = $registered[0]; 131 | } else { 132 | if ( ! $client->check_redirect_uri( $redirect_uri ) ) { 133 | return new WP_Error( 134 | 'oauth2.types.authorization_code.handle_authorisation.invalid_redirect_uri', 135 | __( 'Specified redirect URI is not valid for this client.', 'oauth2' ) 136 | ); 137 | } 138 | } 139 | 140 | return $redirect_uri; 141 | } 142 | 143 | /** 144 | * Render the authorisation form. 145 | * 146 | * @param Client $client Client being authorised. 147 | * @param WP_Error $errors Errors to display, if any. 148 | */ 149 | protected function render_form( Client $client, WP_Error $errors = null ) { 150 | $file = locate_template( 'oauth2-authorize.php' ); 151 | if ( empty( $file ) ) { 152 | $file = dirname( dirname( __DIR__ ) ) . '/theme/oauth2-authorize.php'; 153 | } 154 | 155 | include $file; 156 | } 157 | 158 | /** 159 | * Get the nonce action for a client. 160 | * 161 | * @param Client $client Client to generate nonce for. 162 | * @return string Nonce action for given client. 163 | */ 164 | protected function get_nonce_action( Client $client ) { 165 | return sprintf( 'oauth2_authorize:%s', $client->get_id() ); 166 | } 167 | 168 | /** 169 | * Filter the redirection args. 170 | * 171 | * @param array $redirect_args Redirect args. 172 | * @param boolean $authorized True if authorized, false otherwise. 173 | * @param Client $client Client being authorised. 174 | * @param array $data Data for the request. 175 | */ 176 | protected function filter_redirect_args( $redirect_args, $authorized, Client $client, $data ) { 177 | if ( ! $authorized ) { 178 | /** 179 | * Filter the redirect args when the user has cancelled. 180 | * 181 | * @param array $redirect_args Redirect args. 182 | * @param Client $client Client being authorised. 183 | * @param array $data Data for the request. 184 | */ 185 | return apply_filters( 'oauth2.redirect_args.cancelled', $redirect_args, $client, $data ); 186 | } 187 | 188 | /** 189 | * Filter the redirect args when the user has authorized. 190 | * 191 | * @param array $redirect_args Redirect args. 192 | * @param Client $client Client being authorised. 193 | * @param array $data Data for the request. 194 | */ 195 | return apply_filters( 'oauth2.redirect_args.authorized', $redirect_args, $client, $data ); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /inc/admin/profile/namespace.php: -------------------------------------------------------------------------------- 1 | get_client(); 39 | } 40 | ); 41 | 42 | if ( ! IS_PROFILE_PAGE ) { 43 | $personal_url = PersonalTokens\get_page_url( [ 'user_id' => $user->ID ] ); 44 | } else { 45 | $personal_url = PersonalTokens\get_page_url(); 46 | } 47 | 48 | ?> 49 |

50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 64 | 65 | 66 | 67 | 72 | 73 | 74 |
68 | 69 | 70 | 71 |
75 | 76 |

77 |

78 | 79 |

80 | 81 | get_client(); 92 | $is_personal = $client instanceof PersonalClient; 93 | 94 | if ( $is_personal ) { 95 | $token_name = $token->get_meta( 'name', __( 'Unknown Token', 'oauth2' ) ); 96 | } 97 | 98 | $creation_time = $token->get_creation_time(); 99 | $details = [ 100 | sprintf( 101 | /* translators: %1$s: formatted date, %2$s: formatted time */ 102 | esc_html__( 'Authorized %1$s at %2$s', 'oauth2' ), 103 | date( get_option( 'date_format' ), $creation_time ), 104 | date( get_option( 'time_format' ), $creation_time ) 105 | ), 106 | ]; 107 | 108 | /** 109 | * Filter details shown for an access token on the profile screen. 110 | * 111 | * @param string[] $details List of HTML snippets to render in table. 112 | * @param Access_Token $token Token being displayed. 113 | * @param WP_User $user User whose profile is being rendered. 114 | */ 115 | $details = apply_filters( 'oauth2.admin.profile.render_token_row.details', $details, $token, $user ); 116 | 117 | // Build actions. 118 | if ( $is_personal ) { 119 | $button_title = sprintf( 120 | /* translators: %s: personal token name */ 121 | __( 'Revoke personal token "%s"', 'oauth2' ), 122 | esc_html( $token_name ) 123 | ); 124 | } else { 125 | $button_title = sprintf( 126 | /* translators: %s: app name */ 127 | __( 'Revoke access for "%s"', 'oauth2' ), 128 | $client->get_name() 129 | ); 130 | } 131 | 132 | $actions = [ 133 | sprintf( 134 | '', 135 | esc_attr( $button_title ), 136 | wp_create_nonce( 'oauth2_revoke:' . $token->get_key() ) . ':' . esc_attr( $token->get_key() ), 137 | esc_html__( 'Revoke', 'oauth2' ) 138 | ), 139 | ]; 140 | 141 | /** 142 | * Filter actions shown for an access token on the profile screen. 143 | * 144 | * @param string[] $actions List of HTML snippets to render in table. 145 | * @param Access_Token $token Token being displayed. 146 | * @param WP_User $user User whose profile is being rendered. 147 | */ 148 | $actions = apply_filters( 'oauth2.admin.profile.render_token_row.actions', $actions, $token, $user ); 149 | 150 | $name = sprintf( '%s', $client->get_name() ); 151 | if ( $is_personal ) { 152 | $name = sprintf( 153 | '%s (%s)', 154 | esc_html( $token_name ), 155 | $client->get_name() 156 | ); 157 | } 158 | ?> 159 | 160 | 161 |

162 |

163 | 164 | 165 | 166 | 167 | 168 |

' . esc_html__( 'Token revoked.', 'oauth2' ) . '

'; 182 | } 183 | if ( ! empty( $_GET['oauth2_revocation_failed'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended 184 | echo '

' . esc_html__( 'Unable to revoke token.', 'oauth2' ) . '

'; 185 | } 186 | } 187 | 188 | /** 189 | * Handle a revocation. 190 | * 191 | * @param int $user_id 192 | */ 193 | function handle_revocation( $user_id ) { 194 | if ( empty( $_POST['oauth2_revoke'] ) ) { 195 | return; 196 | } 197 | 198 | $data = sanitize_text_field( wp_unslash( $_POST['oauth2_revoke'] ) ); 199 | if ( strpos( $data, ':' ) === null ) { 200 | return; 201 | } 202 | 203 | // Split out nonce and check it. 204 | list( $nonce, $key ) = explode( ':', $data, 2 ); 205 | if ( ! wp_verify_nonce( $nonce, 'oauth2_revoke:' . $key ) ) { 206 | wp_nonce_ays( 'oauth2_revoke' ); 207 | die(); 208 | } 209 | 210 | $token = Access_Token::get_by_id( $key ); 211 | if ( empty( $token ) ) { 212 | wp_safe_redirect( add_query_arg( 'oauth2_revocation_failed', true, get_edit_user_link( $user_id ) ) ); 213 | exit; 214 | } 215 | 216 | // Check it's for the right user. 217 | if ( $token->get_user_id() !== $user_id ) { 218 | wp_die(); 219 | } 220 | 221 | $result = $token->revoke(); 222 | if ( is_wp_error( $result ) ) { 223 | wp_safe_redirect( add_query_arg( 'oauth2_revocation_failed', true, get_edit_user_link( $user_id ) ) ); 224 | exit; 225 | } 226 | 227 | // Success, redirect and tell the user. 228 | wp_safe_redirect( add_query_arg( 'oauth2_revoked', $key, get_edit_user_link( $user_id ) ) ); 229 | exit; 230 | } 231 | -------------------------------------------------------------------------------- /inc/class-client.php: -------------------------------------------------------------------------------- 1 | post = $post; 40 | } 41 | 42 | /** 43 | * Get the client's ID. 44 | * 45 | * @return string Client ID. 46 | */ 47 | public function get_id() { 48 | return $this->post->post_name; 49 | } 50 | 51 | /** 52 | * Get the client's post ID. 53 | * 54 | * For internal (WordPress) use only. For external use, use get_key() 55 | * 56 | * @return int Client ID. 57 | */ 58 | public function get_post_id() { 59 | return $this->post->ID; 60 | } 61 | 62 | /** 63 | * Get the client's name. 64 | * 65 | * @return string HTML string. 66 | */ 67 | public function get_name() { 68 | return get_the_title( $this->get_post_id() ); 69 | } 70 | 71 | /** 72 | * Get the client's description. 73 | * 74 | * @param boolean $raw True to get raw database value for editing, false to get rendered value for display. 75 | * 76 | * @return string 77 | */ 78 | public function get_description( $raw = false ) { 79 | // Replicate the_content()'s filters. 80 | global $post; 81 | $current_post = $post; 82 | $the_post = get_post( $this->get_post_id() ); 83 | if ( $raw ) { 84 | // Skip the filtering and globals. 85 | return $the_post->post_content; 86 | } 87 | 88 | // Set up globals so the filters have context. 89 | $post = $the_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 90 | setup_postdata( $post ); 91 | $content = get_the_content(); 92 | 93 | /** This filter is documented in wp-includes/post-template.php */ 94 | $content = apply_filters( 'the_content', $content ); 95 | $content = str_replace( ']]>', ']]>', $content ); 96 | 97 | // Restore previous post. 98 | $post = $current_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 99 | if ( $post ) { 100 | setup_postdata( $post ); 101 | } 102 | 103 | return $content; 104 | } 105 | 106 | /** 107 | * Get the client's type. 108 | * 109 | * @return string Type ID if available, or an empty string. 110 | */ 111 | public function get_type() { 112 | return get_post_meta( $this->get_post_id(), static::TYPE_KEY, true ); 113 | } 114 | 115 | /** 116 | * Get the Client Secret Key. 117 | * 118 | * @return string The Secret Key if available, or an empty string. 119 | */ 120 | public function get_secret() { 121 | return get_post_meta( $this->get_post_id(), static::CLIENT_SECRET_KEY, true ); 122 | } 123 | 124 | /** 125 | * Get registered URI for the client. 126 | * 127 | * @return array List of valid redirect URIs. 128 | */ 129 | public function get_redirect_uris() { 130 | return (array) get_post_meta( $this->get_post_id(), static::REDIRECT_URI_KEY, true ); 131 | } 132 | 133 | /** 134 | * Validate a callback URL. 135 | * 136 | * Based on {@see wp_http_validate_url}, but less restrictive around ports 137 | * and hosts. In particular, it allows any scheme, host or port rather than 138 | * just HTTP with standard ports. 139 | * 140 | * @param string $url URL for the callback. 141 | * 142 | * @return bool True for a valid callback URL, false otherwise. 143 | */ 144 | public static function validate_callback( $url ) { 145 | if ( strpos( $url, ':' ) === false ) { 146 | return false; 147 | } 148 | 149 | $parsed_url = wp_parse_url( $url ); 150 | if ( ! $parsed_url || empty( $parsed_url['host'] ) ) { 151 | return false; 152 | } 153 | 154 | if ( isset( $parsed_url['user'] ) || isset( $parsed_url['pass'] ) ) { 155 | return false; 156 | } 157 | 158 | if ( false !== strpbrk( $parsed_url['host'], ':#?[]' ) ) { 159 | return false; 160 | } 161 | 162 | return true; 163 | } 164 | 165 | /** 166 | * Check if a redirect URI is valid for the client. 167 | * 168 | * @param string $uri Supplied redirect URI to check. 169 | * 170 | * @return boolean True if the URI is valid, false otherwise. 171 | * @todo Implement this properly :) 172 | * 173 | */ 174 | public function check_redirect_uri( $uri ) { 175 | if ( ! $this->validate_callback( $uri ) ) { 176 | return false; 177 | } 178 | 179 | $supplied = wp_parse_url( $uri ); 180 | $all_registered = $this->get_redirect_uris(); 181 | 182 | foreach ( $all_registered as $registered_uri ) { 183 | $registered = wp_parse_url( $registered_uri ); 184 | 185 | // Double-check registered URI is valid. 186 | if ( ! $registered ) { 187 | continue; 188 | } 189 | 190 | // Check all components except query and fragment 191 | $parts = [ 'scheme', 'host', 'port', 'user', 'pass', 'path' ]; 192 | $valid = true; 193 | foreach ( $parts as $part ) { 194 | if ( isset( $registered[ $part ] ) !== isset( $supplied[ $part ] ) ) { 195 | $valid = false; 196 | break; 197 | } 198 | 199 | if ( ! isset( $registered[ $part ] ) ) { 200 | continue; 201 | } 202 | 203 | if ( $registered[ $part ] !== $supplied[ $part ] ) { 204 | $valid = false; 205 | break; 206 | } 207 | } 208 | 209 | /** 210 | * Filter whether a callback is counted as valid. (deprecated). 211 | * User rest_oauth_check_callback. 212 | * 213 | * @param boolean $valid True if the callback URL is valid, false otherwise. 214 | * @param string $url Supplied callback URL. 215 | * @param string $registered_uri URI being checked. 216 | * @param Client $client OAuth 2 client object. 217 | */ 218 | $valid = apply_filters( 'rest_oauth.check_callback', $valid, $uri, $registered_uri, $this ); 219 | 220 | if ( $valid ) { 221 | // Stop checking, we have a match. 222 | return true; 223 | } 224 | } 225 | 226 | return false; 227 | } 228 | 229 | /** 230 | * @param WP_User $user 231 | * 232 | * @return Authorization_Code|WP_Error 233 | */ 234 | public function generate_authorization_code( WP_User $user ) { 235 | return Authorization_Code::create( $this, $user ); 236 | } 237 | 238 | /** 239 | * Get data stored for an authorization code. 240 | * 241 | * @param string $code Authorization code to fetch. 242 | * 243 | * @return Authorization_Code|WP_Error Data if available, error if invalid code. 244 | */ 245 | public function get_authorization_code( $code ) { 246 | return Authorization_Code::get_by_code( $this, $code ); 247 | } 248 | 249 | /** 250 | * @return bool|WP_Error 251 | */ 252 | public function regenerate_secret() { 253 | $result = update_post_meta( $this->get_post_id(), static::CLIENT_SECRET_KEY, wp_generate_password( static::CLIENT_SECRET_LENGTH, false ) ); 254 | if ( ! $result ) { 255 | return new WP_Error( 'oauth2.client.create.failed_meta', __( 'Could not regenerate the client secret.', 'oauth2' ) ); 256 | } 257 | 258 | return true; 259 | } 260 | 261 | /** 262 | * Issue token for a user. 263 | * 264 | * @param \WP_User $user 265 | * @param array $meta 266 | * 267 | * @return Access_Token 268 | */ 269 | public function issue_token( WP_User $user, $meta = [] ) { 270 | return Tokens\Access_Token::create( $this, $user, $meta ); 271 | } 272 | 273 | /** 274 | * Get a client by ID. 275 | * 276 | * @param string $id Client ID. 277 | * 278 | * @return static|null Token if ID is found, null otherwise. 279 | */ 280 | public static function get_by_id( $id ) { 281 | $args = [ 282 | 'post_type' => static::POST_TYPE, 283 | 'post_status' => 'publish', 284 | 'posts_per_page' => 1, 285 | 'no_found_rows' => true, 286 | 287 | // Query by slug. 288 | 'name' => $id, 289 | ]; 290 | $query = new WP_Query( $args ); 291 | if ( empty( $query->posts ) ) { 292 | return null; 293 | } 294 | 295 | return new static( $query->posts[0] ); 296 | } 297 | 298 | /** 299 | * Get a client by post ID. 300 | * 301 | * @param int $id Client/post ID. 302 | * 303 | * @return static|null Client instance on success, null if invalid/not found. 304 | */ 305 | public static function get_by_post_id( $id ) { 306 | $post = get_post( $id ); 307 | if ( ! $post ) { 308 | return null; 309 | } 310 | 311 | return new static( $post ); 312 | } 313 | 314 | /** 315 | * Create a new client. 316 | * 317 | * @param array $data { 318 | * } 319 | * 320 | * @return WP_Error|Client Client instance on success, error otherwise. 321 | */ 322 | public static function create( $data ) { 323 | $client_id = wp_generate_password( static::CLIENT_ID_LENGTH, false ); 324 | $post_data = [ 325 | 'post_type' => static::POST_TYPE, 326 | 'post_title' => $data['name'], 327 | 'post_content' => $data['description'], 328 | 'post_name' => $client_id, 329 | 'post_status' => 'draft', 330 | ]; 331 | 332 | $post_id = wp_insert_post( wp_slash( $post_data ), true ); 333 | if ( is_wp_error( $post_id ) ) { 334 | return $post_id; 335 | } 336 | 337 | // Generate ID and secret. 338 | $meta = [ 339 | static::REDIRECT_URI_KEY => $data['meta']['callback'], 340 | static::TYPE_KEY => $data['meta']['type'], 341 | static::CLIENT_SECRET_KEY => wp_generate_password( static::CLIENT_SECRET_LENGTH, false ), 342 | ]; 343 | 344 | foreach ( $meta as $key => $value ) { 345 | $result = update_post_meta( $post_id, wp_slash( $key ), wp_slash( $value ) ); 346 | if ( ! $result ) { 347 | // Failed, rollback. 348 | return new WP_Error( 'oauth2.client.create.failed_meta', __( 'Could not save meta value.', 'oauth2' ) ); 349 | } 350 | } 351 | 352 | $post = get_post( $post_id ); 353 | 354 | return new static( $post ); 355 | } 356 | 357 | /** 358 | * @param array $data 359 | * 360 | * @return WP_Error|Client Client instance on success, error otherwise. 361 | */ 362 | public function update( $data ) { 363 | $post_data = [ 364 | 'ID' => $this->get_post_id(), 365 | 'post_type' => static::POST_TYPE, 366 | 'post_title' => $data['name'], 367 | 'post_content' => $data['description'], 368 | ]; 369 | 370 | $post_id = wp_update_post( wp_slash( $post_data ), true ); 371 | if ( is_wp_error( $post_id ) ) { 372 | return $post_id; 373 | } 374 | 375 | $meta = [ 376 | static::REDIRECT_URI_KEY => $data['meta']['callback'], 377 | static::TYPE_KEY => $data['meta']['type'], 378 | ]; 379 | 380 | foreach ( $meta as $key => $value ) { 381 | update_post_meta( $post_id, wp_slash( $key ), wp_slash( $value ) ); 382 | } 383 | 384 | $post = get_post( $post_id ); 385 | 386 | return new static( $post ); 387 | } 388 | 389 | /** 390 | * Delete the client. 391 | * 392 | * @return bool 393 | */ 394 | public function delete() { 395 | return (bool) wp_delete_post( $this->get_post_id(), true ); 396 | } 397 | 398 | /** 399 | * Approve a client. 400 | * 401 | * @return bool|WP_Error True if client was updated, error otherwise. 402 | */ 403 | public function approve() { 404 | $data = [ 405 | 'ID' => $this->get_post_id(), 406 | 'post_status' => 'publish', 407 | ]; 408 | $result = wp_update_post( wp_slash( $data ), true ); 409 | 410 | return is_wp_error( $result ) ? $result : true; 411 | } 412 | 413 | /** 414 | * Register the underlying post type. 415 | */ 416 | public static function register_type() { 417 | register_post_type( 418 | static::POST_TYPE, 419 | [ 420 | 'public' => false, 421 | 'hierarchical' => true, 422 | 'capability_type' => [ 423 | 'oauth2_client', 424 | 'oauth2_clients', 425 | ], 426 | 'capabilities' => [ 427 | 'edit_posts' => 'edit_users', 428 | 'edit_others_posts' => 'edit_users', 429 | 'publish_posts' => 'edit_users', 430 | ], 431 | 'supports' => [ 432 | 'title', 433 | 'editor', 434 | 'revisions', 435 | 'author', 436 | 'thumbnail', 437 | ], 438 | ] 439 | ); 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /docs/spec.md: -------------------------------------------------------------------------------- 1 | # OAuth Authentication API 2 | The following document describes the HTTP API for authenticating and authorizing 3 | a remote client with a WordPress installation. 4 | 5 | ## Framework 6 | The WordPress OAuth Authentication API ("OAuth API") is a HTTP-based API based 7 | on the [OAuth 1.0a][RFC5849]. It also builds on the OAuth 1.0a specification 8 | with custom parameters. 9 | 10 | This document describes OAuth API version 0.1. 11 | 12 | ## Terminology 13 | * "access token": A long-lived token used for accessing the site. Grants 14 | permissions to the client based on its scope. 15 | * "client": A software program that accesses the OAuth API and provides services 16 | to a user. 17 | * "request token": A short-lived token used during the OAuth process. Does not 18 | grant any permissions, and can only be used for the authorization steps. 19 | * "site": A WordPress installation providing the OAuth API as a service 20 | * "user": An end-user of an API client. Typically a registered user on the site. 21 | 22 | Note that any relative URLs are taken as relative to the site's base URL. 23 | 24 | ## Motivation 25 | The OAuth API is motivated by three main factors: 26 | 27 | * The user should only ever enter their credentials into the site. Clients 28 | should not only be discouraged from asking for user credentials, but the site 29 | should also avoid providing a way to use them. 30 | 31 | * The API must work on any site. The API must only use features available to the 32 | majority of sites in order to provide a useful utility. 33 | 34 | * The API should be simple to implement in clients. Developers should be able to 35 | create clients by reusing existing libraries, rather than writing full 36 | custom solutions. 37 | 38 | ## Differences from OAuth 1.0a 39 | The OAuth API extends OAuth 1.0a to provide additional functionality. The 40 | following differences apply: 41 | 42 | * The authorization endpoint ("Resource Owner authorization endpoint") MAY 43 | accept a `wp_scope` parameter, based on the OAuth 2.0 `scope` parameter. 44 | (See Step 2: Authorization) 45 | 46 | ## Step 0: Assessing Availability 47 | Before beginning the authorization process, clients SHOULD assess whether the 48 | site supports it. Due to the customizable nature of sites, this is not 49 | guaranteed, as the OAuth API can be disabled or replaced. 50 | 51 | To fulfill this requirement, the OAuth API interfaces with the WordPress JSON 52 | REST API ("WP API"). Most clients using the OAuth API are expected to have the 53 | ability to access the WP API. 54 | 55 | The OAuth API exposes information on itself via the index endpoint of the WP 56 | API, typically available at `/wp-json/`. The WP API is discoverable via the 57 | RSD mechanism, and OAuth API clients using this data SHOULD use the RSD 58 | mechanism, as described by the WP API documentation. 59 | 60 | ### Request 61 | The client sends a HTTP GET request to the index endpoint of the WP API. The 62 | location of the index endpoint is out of scope of this document, and is handled 63 | by the WP API documentation. 64 | 65 | ### Response 66 | The WP API index endpoint returns a JSON object of data relating to the site. 67 | The OAuth API exposes data through the `authentication` value in the `oauth1` 68 | property value. 69 | 70 | The `oauth1` value ("API Description object") is a JSON object with the 71 | following properties defined: 72 | 73 | * `request` An absolute URL giving the location of the "Temporary Credential 74 | Request endpoint" (see Step 1: Request Tokens) 75 | * `authorize`: An absolute URL giving the location of the "Resource Owner 76 | Authorization endpoint" (see Step 2: Authorization) 77 | * `access`: An absolute URL giving the location of the "Token Request endpoint" 78 | (see Step 3: Access Tokens) 79 | * `version`: A version string indicating the version of the OAuth API supported 80 | by the site. 81 | 82 | ## Step 1: Request Tokens 83 | The first step to the authorization process is to obtain a request token. This 84 | step asks the site to issue a temporary token, used only for the authorization 85 | process. This token is a short-expiry token which is not yet linked to a user. 86 | 87 | This request follows the [Temporary Credentials][oauth-request] section of 88 | [RFC5948][]. 89 | 90 | ### Request 91 | The client sends a HTTP POST request to `/oauth1/request` (the "Temporary 92 | Credential Request endpoint"). This URL is also available via the API 93 | Description object as the `request` property, and clients SHOULD use the URL 94 | from the API Description object instead of hardcoding the URL. 95 | 96 | This request should match the format as described in the OAuth 1.0a 97 | specification, section 2.1. 98 | 99 | This request can also contain the following parameters as an extension on top of 100 | the OAuth 1.0a parameters: 101 | 102 | * `wp_scope`: This is a space- or comma-separated field in the style of OAuth 103 | 2.0's scope field. This represents a narrowing of the available permissions to 104 | the client. See Authorization Scope. This parameter is OPTIONAL, and defaults 105 | to "*" (all permissions). 106 | 107 | ### Response 108 | The OAuth API returns a URL-encoded response of the OAuth request token data, as 109 | described in the OAuth 1.0a specification, section 2.1. 110 | 111 | ## Step 2: Authorization 112 | The second step to the authorization process is to request authorization from 113 | the user. This step sends the user to the site, where the user then 114 | authenticates and grants the requested permissions to the client. This 115 | acceptance is stored with the request data on the site. 116 | 117 | This request follows the [Resource Owner Authorization][oauth-authorize] section 118 | of [RFC5849][], with additions. 119 | 120 | ### Request 121 | The client sends the user to `/oauth1/authorize` (the "Resource Owner 122 | Authorization endpoint"). This URL is also available via the API Description 123 | object as the `authorize` property, and clients SHOULD use the URL from the API 124 | Description object instead of hardcoding the URL. 125 | 126 | This request should match the format as described in the OAuth 1.0a 127 | specification, section 2.2. 128 | 129 | This request can also contain the following parameters as an extension on top of 130 | the OAuth 1.0a parameters: 131 | 132 | * `wp_scope`: This is a space- or comma-separated field in the style of OAuth 133 | 2.0's scope field. This represents a narrowing of the available permissions to 134 | the client. See Authorization Scope. This parameter is OPTIONAL, and defaults 135 | to either the `wp_scope` parameter as specified in the Request Token request, 136 | or "*" (all permissions) otherwise. 137 | 138 | ### Response 139 | The site will redirect the user back to the `oauth_callback` as provided in the 140 | Authorization step. The `oauth_token` and `oauth_verifier` parameters will be 141 | appended to the callback URL as per the OAuth 1.0a standard. 142 | 143 | In addition, a `wp_scope` parameter will be appended describing the actual scope 144 | granted (see Authorization Scope). 145 | 146 | ## Step 3: Access Tokens 147 | The third step to the authorization process is to use the now-authorized request 148 | token to request an access token. This step asks the site to grant the client an 149 | access token to use in future requests as authentication, using the request 150 | token. 151 | 152 | This request follows the [Token Credentials][oauth-access] section of 153 | [RFC5849][]. 154 | 155 | ### Request 156 | The client sends the user to `/oauth1/access` (the "Token Request" endpoint). 157 | This URL is also available via the API Description object as the "access" 158 | property, and clients SHOULD use the URL from the API Description object instead 159 | of hardcoding the URL. 160 | 161 | This request should match the format as described in the OAuth 1.0a 162 | specification, section 2.3. 163 | 164 | ### Response 165 | The OAuth API returns a URL-encoded response of the OAuth access token data, as 166 | described in the OAuth 1.0a specification, section 2.3. 167 | 168 | ## Authenticated Requests 169 | ... 170 | 171 | ## Authorization Scope 172 | The OAuth API supports an additional parameter during both the Request Token 173 | request and Authorization request. This `wp_scope` parameter is a list of 174 | delimited strings of requested scopes. Scopes SHOULD be delimited by U+0020 175 | SPACE characters, URL-encoded as `%20`. Clients MAY use U+0020 SPACE characters, 176 | URL-encoded as `+`, or U+002C COMMA characters, URL-encoded as `%2c`. 177 | 178 | The OAuth API will also return the `wp_scope` parameter to the callback URL 179 | during the Authorization step, as a list of space-delimited strings of granted 180 | scopes (U+0020 SPACE characters are encoded as `%20`). This response parameter 181 | indicates the scope granted to the client for the token. This granted scope is 182 | strictly equal to or less permissive than the requested scope; that is, clients 183 | will never be granted additional permissions from those requested, but users may 184 | restrict the client's scope further. 185 | 186 | The default scope for clients that do not specify the `wp_scope` parameter is 187 | `*`, indicating all permissions will be granted. This permission grants the 188 | ability to perform any action the user has the capability to perform, including 189 | any future capabilities they may be granted. This scope SHOULD be used 190 | sparingly, as it presents a large attack surface. 191 | 192 | ### Available Scopes 193 | The following scopes are available: 194 | 195 | * `read`: Ability to read any public site data, or private data that the user 196 | has access to (such as privately published posts). 197 | 198 | Maps to: 199 | * `read` 200 | * `read_private_*` (requires Editor or above) 201 | 202 | * `edit`: Ability to edit any public site data, or private data that the user 203 | has access to. Implies `read`. 204 | 205 | Requires Contributor or above. 206 | 207 | Maps to: 208 | * `edit_*` 209 | * `delete_*` 210 | * `upload_files` (requires Author or above) 211 | * `moderate_comments` (requires Editor or above) 212 | * `manage_categories` (requires Editor or above) 213 | * `edit_others_*` (requires Editor or above) 214 | * `edit_private_*` (requires Editor or above) 215 | * `edit_published_*` (requires Editor or above) 216 | * `delete_others_*` (requires Editor or above) 217 | * `delete_private_*` (requires Editor or above) 218 | * `delete_published_*` (requires Editor or above) 219 | 220 | * `user.read`: Ability to read most user data, with the exception of the user's 221 | email address. 222 | 223 | * `user.email`: Ability to read the user's email address. Use of the user's 224 | email address should conform to all local laws (for both the client and site) 225 | with regards to spam. Implies `user.read`. 226 | 227 | * `user.edit`: Ability to edit any user data. Implies `user.read` 228 | and `user.email`. 229 | 230 | * `admin.read`: Ability to read admin-only data. 231 | 232 | Requires Admin or Super Admin. 233 | 234 | Maps to: 235 | * `list_users` 236 | 237 | * `admin.edit`: Ability to edit admin-only data. 238 | 239 | Requires Admin or Super Admin. 240 | 241 | Maps to: 242 | * `manage_options` 243 | * `install_plugins` 244 | * `update_plugins` 245 | * `install_themes` 246 | * `switch_themes` 247 | * `update_themes` 248 | * `edit_theme_options` 249 | * `update_core` 250 | * `edit_dashboard` 251 | 252 | * `admin.users`: Ability to administrate users. 253 | 254 | Requires Admin or Super Admin. Implies `user.edit`. 255 | 256 | Maps to: 257 | * `list_users` 258 | * `create_users` 259 | * `edit_users` 260 | * `promote_users` 261 | * `remove_users` 262 | * `delete_users` 263 | 264 | * `admin.import`: Ability to import data. 265 | 266 | Requires Admin or Super Admin. Implies `edit`. 267 | 268 | Maps to: 269 | * `import` 270 | 271 | * `admin.export`: Ability to export data. 272 | 273 | Requires Admin or Super Admin. Implies `read`. 274 | 275 | Maps to: 276 | * `export` 277 | 278 | For most applications, `read` and `user.read` are appropriate. For any 279 | applications which need access to information about the current user, 280 | `user.read` is recommended. 281 | 282 | Any permissions requested that are not available to the current user will cause 283 | an error to be returned to the client. Note that for permissions like `edit`, 284 | users without the `upload_files` capability (e.g.) will **not** cause an error, 285 | as the permission encompasses other capabilities. A user without the `edit_*` 286 | capability **will** cause an error, however. 287 | 288 | [RFC5849]: http://tools.ietf.org/html/rfc5849 289 | [oauth-request]: http://tools.ietf.org/html/rfc5849#section-2.1 290 | [oauth-authorize]: http://tools.ietf.org/html/rfc5849#section-2.2 291 | [oauth-access]: http://tools.ietf.org/html/rfc5849#section-2.3 292 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 51 Franklin St, Fifth Floor, Boston, MA 02110, USA 6 | 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | Preamble 11 | 12 | The licenses for most software are designed to take away your 13 | freedom to share and change it. By contrast, the GNU General Public 14 | License is intended to guarantee your freedom to share and change free 15 | software--to make sure the software is free for all its users. This 16 | General Public License applies to most of the Free Software 17 | Foundation's software and to any other program whose authors commit to 18 | using it. (Some other Free Software Foundation software is covered by 19 | the GNU Library General Public License instead.) You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | this service if you wish), that you receive source code or can get it 26 | if you want it, that you can change the software or use pieces of it 27 | in new free programs; and that you know you can do these things. 28 | 29 | To protect your rights, we need to make restrictions that forbid 30 | anyone to deny you these rights or to ask you to surrender the rights. 31 | These restrictions translate to certain responsibilities for you if you 32 | distribute copies of the software, or if you modify it. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must give the recipients all the rights that 36 | you have. You must make sure that they, too, receive or can get the 37 | source code. And you must show them these terms so they know their 38 | rights. 39 | 40 | We protect your rights with two steps: (1) copyright the software, and 41 | (2) offer you this license which gives you legal permission to copy, 42 | distribute and/or modify the software. 43 | 44 | Also, for each author's protection and ours, we want to make certain 45 | that everyone understands that there is no warranty for this free 46 | software. If the software is modified by someone else and passed on, we 47 | want its recipients to know that what they have is not the original, so 48 | that any problems introduced by others will not reflect on the original 49 | authors' reputations. 50 | 51 | Finally, any free program is threatened constantly by software 52 | patents. We wish to avoid the danger that redistributors of a free 53 | program will individually obtain patent licenses, in effect making the 54 | program proprietary. To prevent this, we have made it clear that any 55 | patent must be licensed for everyone's free use or not licensed at all. 56 | 57 | The precise terms and conditions for copying, distribution and 58 | modification follow. 59 | 60 | GNU GENERAL PUBLIC LICENSE 61 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 62 | 63 | 0. This License applies to any program or other work which contains 64 | a notice placed by the copyright holder saying it may be distributed 65 | under the terms of this General Public License. The "Program", below, 66 | refers to any such program or work, and a "work based on the Program" 67 | means either the Program or any derivative work under copyright law: 68 | that is to say, a work containing the Program or a portion of it, 69 | either verbatim or with modifications and/or translated into another 70 | language. (Hereinafter, translation is included without limitation in 71 | the term "modification".) Each licensee is addressed as "you". 72 | 73 | Activities other than copying, distribution and modification are not 74 | covered by this License; they are outside its scope. The act of 75 | running the Program is not restricted, and the output from the Program 76 | is covered only if its contents constitute a work based on the 77 | Program (independent of having been made by running the Program). 78 | Whether that is true depends on what the Program does. 79 | 80 | 1. You may copy and distribute verbatim copies of the Program's 81 | source code as you receive it, in any medium, provided that you 82 | conspicuously and appropriately publish on each copy an appropriate 83 | copyright notice and disclaimer of warranty; keep intact all the 84 | notices that refer to this License and to the absence of any warranty; 85 | and give any other recipients of the Program a copy of this License 86 | along with the Program. 87 | 88 | You may charge a fee for the physical act of transferring a copy, and 89 | you may at your option offer warranty protection in exchange for a fee. 90 | 91 | 2. You may modify your copy or copies of the Program or any portion 92 | of it, thus forming a work based on the Program, and copy and 93 | distribute such modifications or work under the terms of Section 1 94 | above, provided that you also meet all of these conditions: 95 | 96 | a) You must cause the modified files to carry prominent notices 97 | stating that you changed the files and the date of any change. 98 | 99 | b) You must cause any work that you distribute or publish, that in 100 | whole or in part contains or is derived from the Program or any 101 | part thereof, to be licensed as a whole at no charge to all third 102 | parties under the terms of this License. 103 | 104 | c) If the modified program normally reads commands interactively 105 | when run, you must cause it, when started running for such 106 | interactive use in the most ordinary way, to print or display an 107 | announcement including an appropriate copyright notice and a 108 | notice that there is no warranty (or else, saying that you provide 109 | a warranty) and that users may redistribute the program under 110 | these conditions, and telling the user how to view a copy of this 111 | License. (Exception: if the Program itself is interactive but 112 | does not normally print such an announcement, your work based on 113 | the Program is not required to print an announcement.) 114 | 115 | These requirements apply to the modified work as a whole. If 116 | identifiable sections of that work are not derived from the Program, 117 | and can be reasonably considered independent and separate works in 118 | themselves, then this License, and its terms, do not apply to those 119 | sections when you distribute them as separate works. But when you 120 | distribute the same sections as part of a whole which is a work based 121 | on the Program, the distribution of the whole must be on the terms of 122 | this License, whose permissions for other licensees extend to the 123 | entire whole, and thus to each and every part regardless of who wrote it. 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | -------------------------------------------------------------------------------- /inc/admin/namespace.php: -------------------------------------------------------------------------------- 1 | value, or wp_parse_args string. 39 | * 40 | * @return string Requested URL. 41 | */ 42 | function get_url( $params = [] ) { 43 | $url = admin_url( 'users.php' ); 44 | $params = [ 'page' => BASE_SLUG ] + wp_parse_args( $params ); 45 | 46 | return add_query_arg( urlencode_deep( $params ), $url ); 47 | } 48 | 49 | /** 50 | * Get the current page action. 51 | * 52 | * @return string One of 'add', 'edit', 'delete', or '' for default (list) 53 | */ 54 | function get_page_action() { 55 | return isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended 56 | } 57 | 58 | /** 59 | * Load data for our page. 60 | */ 61 | function load() { 62 | switch ( get_page_action() ) { 63 | case 'add': 64 | case 'edit': 65 | render_edit_page(); 66 | break; 67 | 68 | case 'delete': 69 | handle_delete(); 70 | break; 71 | 72 | case 'regenerate': 73 | handle_regenerate(); 74 | break; 75 | 76 | case 'approve': 77 | handle_approve(); 78 | break; 79 | 80 | default: 81 | global $wp_list_table; 82 | 83 | $wp_list_table = new ListTable(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 84 | 85 | $wp_list_table->prepare_items(); 86 | 87 | return; 88 | } 89 | } 90 | 91 | /** 92 | * 93 | */ 94 | function dispatch() { 95 | switch ( get_page_action() ) { 96 | case 'add': 97 | case 'edit': 98 | case 'delete': 99 | case 'approve': 100 | break; 101 | 102 | default: 103 | render(); 104 | break; 105 | } 106 | } 107 | 108 | /** 109 | * Render the list page. 110 | */ 111 | function render() { 112 | global $wp_list_table; 113 | 114 | ?> 115 |
116 |

117 | 122 | 124 | 127 |

128 |

' . esc_html__( 'Deleted application.', 'oauth2' ) . '

'; 131 | } elseif ( ! empty( $_GET['approved'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended 132 | echo '

' . esc_html__( 'Approved application.', 'oauth2' ) . '

'; 133 | } 134 | ?> 135 | 136 | views(); ?> 137 | 138 |
139 | 140 | search_box( esc_html__( 'Search Applications', 'oauth2' ), 'oauth2' ); ?> 141 | 142 | display(); ?> 143 | 144 |
145 | 146 |
147 | 148 | 149 | get_post_id() ); 201 | } 202 | 203 | // Check that the parameters are correct first 204 | $params = validate_parameters( wp_unslash( $_POST ) ); 205 | 206 | if ( is_wp_error( $params ) ) { 207 | $messages[] = $params->get_error_message(); 208 | 209 | return $messages; 210 | } 211 | 212 | if ( empty( $consumer ) ) { 213 | // Create the consumer 214 | $data = [ 215 | 'name' => $params['name'], 216 | 'description' => $params['description'], 217 | 'meta' => [ 218 | 'type' => $params['type'], 219 | 'callback' => $params['callback'], 220 | ], 221 | ]; 222 | 223 | $consumer = Client::create( $data ); 224 | $result = $consumer; 225 | } else { 226 | // Update the existing consumer post 227 | $data = [ 228 | 'name' => $params['name'], 229 | 'description' => $params['description'], 230 | 'meta' => [ 231 | 'type' => $params['type'], 232 | 'callback' => $params['callback'], 233 | ], 234 | ]; 235 | 236 | $result = $consumer->update( $data ); 237 | } 238 | 239 | if ( is_wp_error( $result ) ) { 240 | $messages[] = $result->get_error_message(); 241 | 242 | return $messages; 243 | } 244 | 245 | // Success, redirect to alias page 246 | $location = get_url( 247 | [ 248 | 'action' => 'edit', 249 | 'id' => $consumer->get_post_id(), 250 | 'did_action' => $did_action, 251 | ] 252 | ); 253 | wp_safe_redirect( $location ); 254 | exit; 255 | } 256 | 257 | /** 258 | * Output alias editing page 259 | */ 260 | function render_edit_page() { 261 | if ( ! current_user_can( 'edit_users' ) ) { 262 | wp_die( esc_html__( 'You do not have permission to access this page.', 'oauth2' ) ); 263 | } 264 | 265 | // Are we editing? 266 | $consumer = null; 267 | $form_action = get_url( 'action=add' ); 268 | $regenerate_action = ''; 269 | if ( ! empty( $_REQUEST['id'] ) ) { 270 | $id = absint( $_REQUEST['id'] ); 271 | $consumer = Client::get_by_post_id( $id ); 272 | if ( is_wp_error( $consumer ) || empty( $consumer ) ) { 273 | wp_die( esc_html__( 'Invalid client ID.', 'oauth2' ) ); 274 | } 275 | 276 | $form_action = get_url( 277 | [ 278 | 'action' => 'edit', 279 | 'id' => $id, 280 | ] 281 | ); 282 | $regenerate_action = get_url( 283 | [ 284 | 'action' => 'regenerate', 285 | 'id' => $id, 286 | ] 287 | ); 288 | } 289 | 290 | // Handle form submission 291 | $messages = []; 292 | $form_data = []; 293 | if ( ! empty( $_POST['_wpnonce'] ) ) { 294 | if ( empty( $consumer ) ) { 295 | check_admin_referer( 'rest-oauth2-add' ); 296 | } else { 297 | check_admin_referer( 'rest-oauth2-edit-' . $consumer->get_post_id() ); 298 | } 299 | 300 | $messages = handle_edit_submit( $consumer ); 301 | $form_data = wp_unslash( $_POST ); 302 | } 303 | if ( ! empty( $_GET['did_action'] ) ) { 304 | switch ( $_GET['did_action'] ) { 305 | case 'edit': 306 | $messages[] = esc_html__( 'Updated application.', 'oauth2' ); 307 | break; 308 | 309 | case 'regenerate': 310 | $messages[] = esc_html__( 'Regenerated secret.', 'oauth2' ); 311 | break; 312 | 313 | default: 314 | $messages[] = esc_html__( 'Successfully created application.', 'oauth2' ); 315 | break; 316 | } 317 | } 318 | 319 | $data = []; 320 | 321 | if ( empty( $consumer ) || ! empty( $form_data ) ) { 322 | foreach ( [ 'name', 'description', 'callback', 'type' ] as $key ) { 323 | $data[ $key ] = empty( $form_data[ $key ] ) ? '' : $form_data[ $key ]; 324 | } 325 | } else { 326 | $data['name'] = $consumer->get_name(); 327 | $data['description'] = $consumer->get_description( true ); 328 | $data['type'] = $consumer->get_type(); 329 | $data['callback'] = $consumer->get_redirect_uris(); 330 | 331 | if ( is_array( $data['callback'] ) ) { 332 | $data['callback'] = implode( ',', $data['callback'] ); 333 | } 334 | } 335 | 336 | // Header time! 337 | global $title, $parent_file, $submenu_file; 338 | // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited 339 | $title = $consumer ? esc_html__( 'Edit Application', 'oauth2' ) : esc_html__( 'Add Application', 'oauth2' ); 340 | $parent_file = 'users.php'; 341 | $submenu_file = BASE_SLUG; 342 | // phpcs:enable 343 | 344 | include ABSPATH . 'wp-admin/admin-header.php'; 345 | ?> 346 | 347 |
348 |

349 | 350 |

' . esc_html( $msg ) . '

'; 354 | } 355 | } 356 | ?> 357 | 358 |
359 | 360 | 361 | 364 | 368 | 369 | 370 | 373 | 376 | 377 | 378 | 381 | 425 | 426 | 427 | 430 | 434 | 435 |
362 | 363 | 365 | 366 |

367 |
371 | 372 | 374 | 375 |
379 | 380 | 382 |
    383 |
  • 384 | 390 | /> 391 | 394 |

    395 | 401 |

    402 |
  • 403 |
  • 404 | 410 | /> 411 | 414 |

    415 | 421 |

    422 |
  • 423 |
424 |
428 | 429 | 431 | 432 |

433 |
436 | 437 | get_post_id() ) . '" />'; 444 | wp_nonce_field( 'rest-oauth2-edit-' . $consumer->get_post_id() ); 445 | submit_button( esc_html__( 'Save Client', 'oauth2' ) ); 446 | } 447 | 448 | ?> 449 |
450 | 451 | 452 |
453 |

454 | 455 | 456 | 457 | 460 | 463 | 464 | 465 | 468 | 471 | 472 |
458 | 459 | 461 | get_id() ); ?> 462 |
466 | 467 | 469 | get_secret() ); ?> 470 |
473 | 474 | get_post_id() ); 476 | submit_button( esc_html__( 'Regenerate Secret', 'oauth2' ), 'delete' ); 477 | ?> 478 |
479 | 480 | 481 | 482 | ' . esc_html__( 'Cheatin’ uh?', 'oauth2' ) . '' . 499 | '

' . esc_html__( 'You are not allowed to delete this application.', 'oauth2' ) . '

', 500 | 403 501 | ); 502 | } 503 | 504 | $client = Client::get_by_post_id( $id ); 505 | if ( is_wp_error( $client ) ) { 506 | wp_die( $client ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 507 | 508 | return; 509 | } 510 | 511 | if ( ! $client->delete() ) { 512 | wp_die( esc_html__( 'Invalid client ID' ) ); 513 | 514 | return; 515 | } 516 | 517 | wp_safe_redirect( get_url( 'deleted=1' ) ); 518 | exit; 519 | } 520 | 521 | /** 522 | * Approve the client. 523 | */ 524 | function handle_approve() { 525 | if ( empty( $_GET['id'] ) ) { 526 | return; 527 | } 528 | 529 | $id = absint( $_GET['id'] ); 530 | check_admin_referer( 'rest-oauth2-approve:' . $id ); 531 | 532 | if ( ! current_user_can( 'publish_post', $id ) ) { 533 | wp_die( 534 | '

' . esc_html__( 'Cheatin’ uh?', 'oauth2' ) . '

' . 535 | '

' . esc_html__( 'You are not allowed to approve this application.', 'oauth2' ) . '

', 536 | 403 537 | ); 538 | } 539 | 540 | $client = Client::get_by_post_id( $id ); 541 | if ( is_wp_error( $client ) ) { 542 | wp_die( $client ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 543 | } 544 | 545 | $did_approve = $client->approve(); 546 | if ( is_wp_error( $did_approve ) ) { 547 | wp_die( $did_approve ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 548 | } 549 | 550 | wp_safe_redirect( get_url( 'approved=1' ) ); 551 | exit; 552 | } 553 | 554 | /** 555 | * Regenerate the client secret. 556 | */ 557 | function handle_regenerate() { 558 | if ( empty( $_GET['id'] ) ) { 559 | return; 560 | } 561 | 562 | $id = absint( $_GET['id'] ); 563 | check_admin_referer( 'rest-oauth2-regenerate:' . $id ); 564 | 565 | if ( ! current_user_can( 'edit_post', $id ) ) { 566 | wp_die( 567 | '

' . esc_html__( 'Cheatin’ uh?', 'oauth2' ) . '

' . 568 | '

' . esc_html__( 'You are not allowed to edit this application.', 'oauth2' ) . '

', 569 | 403 570 | ); 571 | } 572 | 573 | $client = Client::get_by_post_id( $id ); 574 | $result = $client->regenerate_secret(); 575 | if ( is_wp_error( $result ) ) { 576 | wp_die( esc_html( $result->get_error_message() ) ); 577 | } 578 | 579 | wp_safe_redirect( 580 | get_url( 581 | [ 582 | 'action' => 'edit', 583 | 'id' => $id, 584 | 'did_action' => 'regenerate', 585 | ] 586 | ) 587 | ); 588 | exit; 589 | } 590 | --------------------------------------------------------------------------------