├── .gitignore ├── languages ├── wp_rest_oauth2-fi.mo ├── wp_rest_oauth2.pot └── wp_rest_oauth2-fi.po ├── lib ├── storage │ ├── class-oa2-access-token.php │ ├── class-oa2-refresh-token.php │ ├── class-oa2-authorization-code.php │ ├── class-oa2-client.php │ ├── class-wp-rest-oauth-client.php │ └── class-oa2-token.php ├── helpers │ ├── class-oa2-scope-helper.php │ ├── class-oa2-header-helper.php │ └── class-oa2-error-helper.php ├── class-oa2-storage-controller.php ├── class-oa2-listtable.php ├── class-oa2-ui.php ├── class-oa2-authorize-controller.php ├── class-oa2-authenticator.php ├── class-oa2-token-controller.php └── class-oa2-admin.php ├── README.md ├── templates └── oauth2-authorize.php ├── admin.php └── oauth2-server.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /languages/wp_rest_oauth2-fi.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apkoponen/wp-rest-api-oauth2/HEAD/languages/wp_rest_oauth2-fi.mo -------------------------------------------------------------------------------- /lib/storage/class-oa2-access-token.php: -------------------------------------------------------------------------------- 1 | = 4.4. 6 | 7 | ## Get Involved 8 | 9 | If you are interested in getting involved with the project, fork the repo, create a feature branch 10 | and do a pull request when you are ready to commit. All pull requests are discussed and if approved 11 | you will be added to the list of collaborators. 12 | 13 | ## Development 14 | 15 | Install a fresh copy of of WordPress and clone down the repo into `/wp-contents/plugins`. You will 16 | also need [WP API](https://github.com/WP-API/WP-API/). 17 | 18 | Feature branches should be prefixed with `feature` and branches that fix should be prefixed with `fix`. 19 | For example, a feature branch that implants WP_CLI could be named `feature-wpcli`. 20 | 21 | ## Development Guidelines 22 | 23 | All development will follow the [WordPress coding standard](https://codex.wordpress.org/WordPress_Coding_Standards). 24 | 25 | * Function naming convention - All functions will need to be prefixed properly. Function prefix is `oauth2_`. 26 | * To keep in-line with WP core ideals, functions should be broke out using wrappers if necessary. More code is drowned 27 | out by cleaner and more scalable code. -------------------------------------------------------------------------------- /lib/class-oa2-storage-controller.php: -------------------------------------------------------------------------------- 1 | client_secret !== $client_secret ) { 37 | return false; 38 | } 39 | 40 | return true; 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /lib/helpers/class-oa2-header-helper.php: -------------------------------------------------------------------------------- 1 | $value ) { 47 | if ( strtolower( $key ) === 'authorization' ) { 48 | return $value; 49 | } 50 | } 51 | } 52 | 53 | return null; 54 | } 55 | 56 | /** 57 | * Get the Bearer token from Authorization header 58 | * 59 | * @return array 60 | */ 61 | public static function get_authorization_bearer() { 62 | $header = self::get_authorization_header(); 63 | $header_params = null; 64 | 65 | if ( !empty( $header ) ) { 66 | // Trim leading spaces 67 | $header = trim( $header ); 68 | 69 | $header_params = self::parse_header( $header ); 70 | } 71 | 72 | return $header_params; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /lib/class-oa2-listtable.php: -------------------------------------------------------------------------------- 1 | get_pagenum(); 12 | 13 | $additional_args = array( 14 | 'paged' => $paged 15 | ); 16 | 17 | $query = OA2_Client::get_clients_query($additional_args); 18 | $this->items = $query->posts; 19 | 20 | $pagination_args = array( 21 | 'total_items' => $query->found_posts, 22 | 'total_pages' => $query->max_num_pages, 23 | 'per_page' => $query->get('posts_per_page') 24 | ); 25 | $this->set_pagination_args($pagination_args); 26 | } 27 | 28 | public function get_columns() { 29 | $c = array( 30 | 'cb' => '', 31 | 'name' => __( 'Name', 'wp_rest_oauth2' ), 32 | 'description' => __( 'Description', 'wp_rest_oauth2' ), 33 | ); 34 | 35 | return $c; 36 | } 37 | 38 | public function column_cb( $item ) { 39 | ?> 40 | 42 | 43 | 45 | 46 | ID ); 51 | if ( empty( $title ) ) { 52 | $title = '' . esc_html__( 'Untitled', 'wp_rest_oauth2' ) . ''; 53 | } 54 | 55 | $edit_link = add_query_arg( 56 | array( 57 | 'page' => 'rest-oauth2-apps', 58 | 'action' => 'edit', 59 | 'id' => $item->ID, 60 | ), 61 | admin_url( 'users.php' ) 62 | ); 63 | $delete_link = add_query_arg( 64 | array( 65 | 'page' => 'rest-oauth2-apps', 66 | 'action' => 'delete', 67 | 'id' => $item->ID, 68 | ), 69 | admin_url( 'users.php' ) 70 | ); 71 | $delete_link = wp_nonce_url( $delete_link, 'rest-oauth2-delete:' . $item->ID ); 72 | 73 | $actions = array( 74 | 'edit' => sprintf( '%s', esc_url( $edit_link ), esc_html__( 'Edit', 'wp_rest_oauth2' ) ), 75 | 'delete' => sprintf( '%s', esc_url( $delete_link ), esc_html__( 'Delete', 'wp_rest_oauth2' ) ), 76 | ); 77 | $action_html = $this->row_actions( $actions ); 78 | 79 | return $title . ' ' . $action_html; 80 | } 81 | 82 | protected function column_description( $item ) { 83 | return $item->post_content; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/storage/class-oa2-authorization-code.php: -------------------------------------------------------------------------------- 1 | redirect_uri ) && $redirect_uri !== $consumer->redirect_uri ) { 45 | return new WP_Error( 'oauth2_redirect_uri_mismatch', __( 'The client redirect URI does not match the provided URI.', 'wp_rest_oauth2' ), array( 'status' => 400 ) ); 46 | } 47 | 48 | // Hash code in order to prevent leaking from DB 49 | $code_hash = self::hash_code($code); 50 | 51 | // Setup data for filtering 52 | $unfiltered_data = array( 53 | 'hash' => $code_hash, 54 | 'user_id' => intval( $user_id ), 55 | 'redirect_uri' => $redirect_uri, 56 | 'client_id' => get_post_meta( $consumer->ID, 'client_id', true ), 57 | 'expires' => intval( $expires ), 58 | 'scope' => $scope 59 | ); 60 | $data = apply_filters( 'wp_rest_oauth2_authorization_code_data', $unfiltered_data ); 61 | 62 | add_option( 'wp_rest_oauth2_code_' . $code_hash , $data ); 63 | 64 | return $code; 65 | } 66 | 67 | /** 68 | * Revoke an authorization code 69 | * 70 | * @param string $code 71 | * @return bool True, if code is successfully deleted. False on failure. 72 | */ 73 | public static function revoke_code( $code ) { 74 | return delete_option( 'wp_rest_oauth2_code_' . self::hash_code( $code ) ); 75 | } 76 | 77 | /** 78 | * Delete expired codes 79 | */ 80 | public static function expire_codes() { 81 | $results = $wpdb->get_col( "SELECT option_value FROM {$wpdb->options} WHERE option_name LIKE 'wp_rest_oauth2_code_%'", 0 ); 82 | $codes = array_map( 'unserialize', $results ); 83 | 84 | foreach ( $codes as $code ) { 85 | if ( $code[ 'expires' ] >= time() ) { 86 | delete_option( 'wp_rest_oauth2_code_' . $code[ 'hash' ] ); 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * Hash a code for DB 93 | * 94 | * @param string $code 95 | * @return string Hash 96 | */ 97 | public static function hash_code( $code ) { 98 | $code_hash = wp_hash($code); 99 | return $code_hash; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /templates/oauth2-authorize.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 50 | 51 |
106 | 107 | 123 | 124 | array( 20 | 'client_secret' => wp_generate_password( self::CONSUMER_SECRET_LENGTH, false ), 21 | ), 22 | ); 23 | 24 | return $this->update( $params ); 25 | } 26 | 27 | /** 28 | * Get the client type. 29 | * 30 | * @return string 31 | */ 32 | protected static function get_type() { 33 | return 'oauth2'; 34 | } 35 | 36 | /** 37 | * Return errors, because we do not use keys 38 | * 39 | * @param type $key 40 | * @return \WP_Error 41 | */ 42 | public static function get_by_key($key) { 43 | return new WP_Error( 'rest_client_keys_not_used', __( 'OAuth 2.0 does not use Client Keys. Use get_by_client_id instead.', 'wp_rest_oauth2' ) ); 44 | } 45 | 46 | /** 47 | * Get a client by client ID. 48 | * 49 | * @param string $type Client type. 50 | * @param string $client_id Client ID. 51 | * @return WP_Post|WP_Error 52 | */ 53 | public static function get_by_client_id( $client_id ) { 54 | $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); 55 | $type = call_user_func( array( $class, 'get_type' ) ); 56 | 57 | $query = new WP_Query(); 58 | $consumers = $query->query( array( 59 | 'post_type' => 'json_consumer', 60 | 'post_status' => 'any', 61 | 'meta_query' => array( 62 | array( 63 | 'key' => 'client_id', 64 | 'value' => $client_id, 65 | ), 66 | array( 67 | 'key' => 'type', 68 | 'value' => $type, 69 | ), 70 | ), 71 | ) ); 72 | 73 | if ( empty( $consumers ) || empty( $consumers[0] ) ) { 74 | return new WP_Error( 'json_consumer_notfound', __( 'Client ID is invalid', 'wp_rest_oauth2' ), array( 'status' => 401 ) ); 75 | } 76 | 77 | return $consumers[0]; 78 | } 79 | 80 | /** 81 | * Get clients 82 | * 83 | * @param array $additional_args WP_Query args 84 | * @return \WP_Query 85 | */ 86 | public static function get_clients_query( $additional_args = array() ) { 87 | $defaults = array( 88 | 'post_type' => 'json_consumer', 89 | 'post_status' => 'any', 90 | 'meta_query' => array( 91 | array( 92 | 'key' => 'type', 93 | 'value' => 'oauth2', 94 | ), 95 | ) 96 | ); 97 | 98 | $args = wp_parse_args( $additional_args, $defaults); 99 | 100 | return new WP_Query($args); 101 | } 102 | 103 | /** 104 | * Get clients 105 | * 106 | * @param array $additional_args WP_Query args 107 | * @return array Array of WP_Posts 108 | */ 109 | public static function get_clients( $additional_args = array() ) { 110 | $query = self::get_clients_query($additional_args); 111 | 112 | return $query->posts; 113 | } 114 | 115 | /** 116 | * Add extra meta to a post. 117 | * 118 | * Adds the client_id and client_secret for a client to the meta on creation. Only adds 119 | * them if they're not set, allowing them to be overridden for consumers 120 | * with a pre-existing pair (such as via an import). 121 | * 122 | * @param array $meta Metadata for the post. 123 | * @param array $params Parameters used to create the post. 124 | * @return array Metadata to actually save. 125 | */ 126 | protected static function add_extra_meta( $meta, $params ) { 127 | if ( empty( $meta['client_id'] ) && empty( $meta['client_secret'] ) ) { 128 | $meta['client_id'] = wp_generate_password( self::CONSUMER_KEY_LENGTH, false ); 129 | $meta['client_secret'] = wp_generate_password( self::CONSUMER_SECRET_LENGTH, false ); 130 | } 131 | return parent::add_extra_meta( $meta, $params ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/class-oa2-ui.php: -------------------------------------------------------------------------------- 1 | render_page(); 48 | if ( is_wp_error( $response ) ) { 49 | $this->display_error( $response ); 50 | } 51 | exit; 52 | } 53 | 54 | /** 55 | * Render authorization page 56 | * 57 | * @return null|WP_Error Null on success, error otherwise 58 | */ 59 | public function render_page() { 60 | 61 | // Check required fields 62 | if ( !OA2_Authorize_Controller::required_params_exist( $_GET ) ) { 63 | $error_code = 'invalid_request'; 64 | return new WP_Error( $error_code, OA2_Error_Helper::get_error_description( $error_code ), array( 'status' => 400 ) ); 65 | } 66 | 67 | // Set up fields 68 | $consumer = OA2_Client::get_by_client_id( $_GET[ 'client_id' ] ); 69 | $scope = OA2_Scope_Helper::get_all_caps_scope(); 70 | if ( !empty( $_GET[ 'scope' ] ) ) { 71 | if(!OA2_Scope_Helper::validate_scope( $_GET[ 'scope' ] )) { 72 | $error_code = 'invalid_scope'; 73 | return new WP_Error( $error_code, OA2_Error_Helper::get_error_description( $error_code ), array( 'status' => 400 ) ); 74 | } 75 | $scope = $_GET[ 'scope' ]; 76 | } 77 | $state = ''; 78 | if ( !empty( $_GET[ 'state' ] ) ) { 79 | $state = $_GET[ 'state' ]; 80 | } 81 | 82 | $this->redirect_uri = esc_url($_GET[ 'redirect_uri' ]); 83 | $this->response_type = sanitize_key($_GET[ 'response_type' ]); 84 | $this->state = $state; 85 | $this->consumer = $consumer; 86 | $this->scope = $scope; 87 | 88 | $file = locate_template( 'oauth2-authorize.php' ); 89 | if ( empty( $file ) ) { 90 | $file = dirname( dirname( __FILE__ ) ) . '/templates/oauth2-authorize.php'; 91 | } 92 | 93 | $errors = array(); 94 | include $file; 95 | } 96 | 97 | /** 98 | * Output required hidden fields 99 | * 100 | * Outputs the required hidden fields for the authorization page, including 101 | * nonce field. 102 | */ 103 | public function page_fields() { 104 | echo ''; 105 | echo ''; 106 | echo ''; 107 | echo ''; 108 | echo ''; 109 | wp_nonce_field( 'oauth2_authorization_' . $this->redirect_uri . '_' . $this->consumer->client_id, '_oauth2_nonce' ); 110 | wp_nonce_field( 'wp_rest' ); 111 | } 112 | 113 | /** 114 | * Display an error using login page wrapper 115 | * 116 | * @param WP_Error $error Error object 117 | */ 118 | public function display_error( WP_Error $error ) { 119 | login_header( __( 'Error', 'wp_rest_oauth2' ), '', $error ); 120 | login_footer(); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /admin.php: -------------------------------------------------------------------------------- 1 | $user->ID, 26 | 'meta_query' => array( 27 | array( 28 | 'key' => 'expires', 29 | 'value' => time(), 30 | 'compare' => '>', 31 | 'type' => 'NUMERIC' 32 | ) 33 | ) 34 | ); 35 | $access_tokens = OA2_Access_Token::get_tokens( $query_args ); 36 | ?> 37 || 41 | |
42 |
43 |
|
67 |
|---|
' . __( 'Token revoked.', 'wp_rest_oauth2' ) . '
'; 81 | } 82 | if ( ! empty( $_GET['wp_rest_oauth2_revocation_failed'] ) ) { 83 | echo '' . __( 'Unable to revoke token.', 'wp_rest_oauth2' ) . '
' . __( 'Token not found.', 'wp_rest_oauth2' ) . '
' . __( 'You are not allowed to edit this token or the token does not exist.', 'wp_rest_oauth2' ) . '
', 104 | 403 105 | ); 106 | } 107 | 108 | $result = OA2_Access_Token::revoke_token( $token[ 'token' ] ); 109 | 110 | if ( is_wp_error( $result ) || $result === false ) { 111 | $redirect = add_query_arg( 'wp_rest_oauth2_revocation_failed', true, get_edit_user_link( $user_id ) ); 112 | } 113 | else { 114 | $redirect = add_query_arg( 'wp_rest_oauth2_revoked', $post_id, get_edit_user_link( $user_id ) ); 115 | } 116 | 117 | wp_redirect($redirect); 118 | exit; 119 | } 120 | -------------------------------------------------------------------------------- /lib/helpers/class-oa2-error-helper.php: -------------------------------------------------------------------------------- 1 | 'access_denied', 19 | * 'error_description' => 'The resource owner or authorization server denied the request.' 20 | ); 21 | */ 22 | public static function get_error( $error_code, $error_description = '' ) { 23 | if ( is_wp_error( $error_description ) ) { 24 | $error_description = array_pop( array_pop( $error_description->errors ) ); 25 | } elseif ( empty( $error_description ) ) { 26 | $error_description = self::get_error_description( $error_code ); 27 | } 28 | $error = array( 29 | 'error' => $error_code, 30 | 'error_description' => $error_description 31 | ); 32 | return $error; 33 | } 34 | 35 | /** 36 | * Get error code default description. 37 | * 38 | * @param string $error_code Code to fetch the description for, e.g. "access_denied" 39 | * @return string Error description if it exists, otherwise empty string. 40 | */ 41 | public static function get_error_description( $error_code ) { 42 | $error_descriptions = array( 43 | 'invalid_request' => __( 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.', 'wp_rest_oauth2' ), 44 | 'unauthorized_client' => __( 'The client is not authorized to request an authorization code using this method.', 'wp_rest_oauth2' ), 45 | 'access_denied' => __( 'The resource owner or authorization server denied the request.', 'wp_rest_oauth2' ), 46 | 'unsupported_response_type' => __( 'The authorization server does not support obtaining an authorization code using this method.', 'wp_rest_oauth2' ), 47 | 'invalid_scope' => __( 'The requested scope is invalid, unknown, or malformed.', 'wp_rest_oauth2' ), 48 | 'server_error' => __( 'The authorization server encountered an unexpected condition that prevented it from fulfilling the request.', 'wp_rest_oauth2' ), 49 | 'temporarily_unavailable' => __( 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.', 'wp_rest_oauth2' ), 50 | 'invalid_client' => __( 'Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method).', 'wp_rest_oauth2' ), 51 | 'invalid_grant' => __( 'The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.', 'wp_rest_oauth2' ), 52 | 'unauthorized_client' => __( 'The authenticated client is not authorized to use this authorization grant type.', 'wp_rest_oauth2' ), 53 | 'unsupported_grant_type' => __( 'The authorization grant type is not supported by the authorization server.', 'wp_rest_oauth2' ), 54 | 'invalid_credentials' => __( 'The user credentials were incorrect.', 'wp_rest_oauth2' ), // Not is Spec 55 | 'ssl_is_required' => __( 'SSL is required', 'wp_rest_oauth2' ) // Not is Spec 56 | ); 57 | $error_description = ( isset( $error_descriptions[$error_code] ) ) ? $error_descriptions[$error_code] : ''; 58 | return $error_description; 59 | } 60 | 61 | /** 62 | * Get default HTTP status code for error code. 63 | * 64 | * @param string $error_code 65 | * @return int Error specific HTTP status code if it exists, otherwise 400. 66 | */ 67 | public static function get_error_status_code( $error_code ) { 68 | $error_descriptions = array( 69 | 'invalid_request' => 400, 70 | 'unauthorized_client' => 400, 71 | 'access_denied' => 401, 72 | 'unsupported_response_type' => 400, 73 | 'invalid_scope' => 400, 74 | 'server_error' => 500, 75 | 'temporarily_unavailable' => 503, 76 | 'invalid_client' => 401, 77 | 'invalid_grant' => 400, 78 | 'unauthorized_client' => 400, 79 | 'unsupported_grant_type' => 400, 80 | 'invalid_credentials' => 401, 81 | 'ssl_is_required' => 403 82 | ); 83 | $error_description = ( isset( $error_descriptions[$error_code] ) ) ? isset( $error_descriptions[$error_code] ) : 400; 84 | return $error_description; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /lib/class-oa2-authorize-controller.php: -------------------------------------------------------------------------------- 1 | get_query_params(); 15 | 16 | // Check that client exists 17 | $consumer = OA2_Client::get_by_client_id( $request_query_params[ 'client_id' ] ); 18 | if ( is_wp_error( $consumer ) ) { 19 | $error = OA2_Error_Helper::get_error( 'invalid_request' ); 20 | return new WP_REST_Response( $error ); 21 | } 22 | 23 | // Check redirect_uri 24 | if ( empty( $request_query_params[ 'redirect_uri' ] ) ) { 25 | $error = OA2_Error_Helper::get_error( 'invalid_request' ); 26 | return new WP_REST_Response( $error ); 27 | } 28 | $redirect_uri = $request_query_params[ 'redirect_uri' ]; 29 | if ( !empty( $consumer->redirect_uri ) && $redirect_uri !== $consumer->redirect_uri ) { 30 | return new WP_Error( 'oauth2_redirect_uri_mismatch', __( 'The client redirect URI does not match the provided URI.', 'wp_rest_oauth2' ), array( 'status' => 400 ) ); 31 | } 32 | 33 | // response_type MUST be 'code' 34 | if ( empty( $request_query_params[ 'response_type' ] ) ) { 35 | $error = OA2_Error_Helper::get_error( 'invalid_request' ); 36 | self::redirect_with_data($redirect_uri, $error); 37 | } 38 | if ( $request_query_params[ 'response_type' ] !== 'code' ) { 39 | $error = OA2_Error_Helper::get_error( 'unsupported_response_type' ); 40 | self::redirect_with_data($redirect_uri, $error); 41 | } 42 | 43 | // Validate scope 44 | if ( !empty( $request_query_params[ 'scope' ] ) && !OA2_Scope_Helper::validate_scope( $request_query_params[ 'scope' ] ) ) { 45 | $error = OA2_Error_Helper::get_error( 'invalid_scope' ); 46 | self::redirect_with_data($redirect_uri, $error); 47 | } 48 | 49 | // Check if we're past authorization 50 | if ( empty( $request_query_params[ 'wp-submit' ] ) ) { 51 | $login_url = site_url( 'wp-login.php?action=oauth2_authorize', 'https' ); 52 | $authorize_url = add_query_arg( array_map( 'rawurlencode', $request_query_params ), $login_url ); 53 | wp_safe_redirect( $authorize_url ); 54 | exit; 55 | } else { 56 | // Check nonce to protect from CSRF (the login has to be active) 57 | check_admin_referer( 'oauth2_authorization_' . $request_query_params[ 'redirect_uri' ] . '_' . $request_query_params[ 'client_id' ], '_oauth2_nonce' ); 58 | // Check if the user authorized the request 59 | if ( $request_query_params[ 'wp-submit' ] !== 'authorize' ) { 60 | $error = OA2_Error_Helper::get_error( 'access_denied' ); 61 | self::redirect_with_data($redirect_uri, $error); 62 | } 63 | } 64 | 65 | // If nonce matches, we know the user. 66 | $user_id = get_current_user_id(); 67 | 68 | // Set scope 69 | $scope = empty( $request_query_params[ 'scope' ] ) ? OA2_Scope_Helper::get_all_caps_scope() : $request_query_params[ 'scope' ]; 70 | 71 | // Create authorization code 72 | $code = OA2_Authorization_Code::generate_code( $request_query_params[ 'client_id' ], $user_id, $request_query_params[ 'redirect_uri' ], time() + 30, $scope ); 73 | 74 | if ( is_wp_error( $code ) ) { 75 | $error = OA2_Error_Helper::get_error( 'invalid_request', $code ); 76 | self::redirect_with_data($redirect_uri, $error); 77 | } 78 | 79 | // if we made it this far, everything has checked out and we redirect with the code. 80 | $data = array( 81 | 'code' => $code 82 | ); 83 | 84 | // If the state is not empty, we need to return it as well 85 | if ( !empty( $request_query_params[ 'state' ] ) ) { 86 | $data[ 'state' ] = $request_query_params[ 'state' ]; 87 | } 88 | 89 | self::redirect_with_data($request_query_params[ 'redirect_uri' ], $data); 90 | } 91 | 92 | /** 93 | * Redirect using data as parameters. 94 | * 95 | * @param type $redirect_uri URI to redirect to. 96 | * @param type $data Data to rawurlencode and add as query args. 97 | */ 98 | static public function redirect_with_data($redirect_uri, $data) { 99 | $urlencoded_data = array_map( 'rawurlencode', $data ); 100 | $redirect_url = add_query_arg( $urlencoded_data, $redirect_uri ); 101 | wp_redirect( $redirect_url ); 102 | exit; 103 | } 104 | 105 | /** 106 | * Checks if required parameters 107 | * 108 | * @param Array $request_query_params Key-value array to check. 109 | * @return Boolean True if all exist 110 | */ 111 | static public function required_params_exist( $request_query_params ) { 112 | $required_params = array( 'client_id', 'response_type', 'redirect_uri' ); 113 | $required_missing = false; 114 | foreach( $required_params as $required_param ) { 115 | if ( empty( $request_query_params[ $required_param ] ) ) { 116 | $required_missing = true; 117 | } 118 | } 119 | $params_exist = !$required_missing; 120 | return $params_exist; 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /lib/class-oa2-authenticator.php: -------------------------------------------------------------------------------- 1 | auth_status !== null) { 38 | return $user; 39 | } 40 | 41 | // Check if token is in request 42 | $bearer_token = OA2_Header_Helper::get_authorization_bearer(); 43 | if ( empty( $bearer_token ) ) { 44 | $this->auth_status = false; 45 | return $user; 46 | } 47 | 48 | // Fetch user by token key 49 | $token = OA2_Access_Token::get_token( $bearer_token ); 50 | if ( is_wp_error( $token ) ) { 51 | $this->auth_status = $token; 52 | return null; 53 | } 54 | 55 | $this->auth_status = true; 56 | $this->authorized_token = $token; 57 | return $token[ 'user_id' ]; 58 | } 59 | 60 | /** 61 | * Force reauthentication after we've registered our handler 62 | * 63 | * We could have checked authentication before OAuth2 was loaded. If so, let's 64 | * try and reauthenticate now that OAuth is loaded. 65 | */ 66 | function force_reauthentication() { 67 | if ( is_user_logged_in() ) { 68 | // Another handler has already worked successfully, no need to 69 | // reauthenticate. 70 | return; 71 | } 72 | 73 | // Force reauthentication 74 | global $current_user; 75 | $current_user = null; 76 | 77 | wp_get_current_user(); 78 | } 79 | 80 | /** 81 | * Report authentication errors to the JSON API 82 | * 83 | * @param WP_Error|mixed value Error from another authentication handler, null if we should handle it, or another value if not 84 | * @return WP_Error|boolean|null {@see WP_JSON_Server::check_authentication} 85 | */ 86 | public function get_authentication_errors( $value ) { 87 | if ( $value !== null ) { 88 | return $value; 89 | } 90 | 91 | return $this->auth_status; 92 | } 93 | 94 | /** 95 | * A filter for user_has_cap that limits the authentication to the scope capabilities. 96 | * 97 | * This is currently the only way to have limited scopes in WordPress. However, it has limitations, as we cannot 98 | * guarantee that the request is made in "the context of the current user". If a plugin is checking the capabilities 99 | * of the user that is same user that was authorized with the token in a REST API request, the capability information 100 | * is wrong. This would be a problem e.g. if has plugin is offers an endpoint for listing the capabilities of all 101 | * users and a request to access the endpoint is made with an access token that has a limited scope. 102 | * 103 | * @param array $allcaps An array of all the user's capabilities. 104 | * @param array $caps Actual capabilities for meta capability. 105 | * @param array $args Optional parameters passed to has_cap(), typically object ID. 106 | * @param WP_User $user The user object. 107 | * @return array An array of capabilities filtered by the token 108 | */ 109 | public function limit_to_scope_caps( $allcaps, $caps, $args, $user ) { 110 | // Filter only if token authorization has been done, the token id matches the token user_id, 111 | // and token has limited scope. 112 | if ( !empty( $this->auth_status ) && !empty( $this->authorized_token ) && 113 | $user->ID === absint($this->authorized_token[ 'user_id' ]) && 114 | $this->authorized_token[ 'scope' ] !== OA2_Scope_Helper::get_all_caps_scope() ) { 115 | $scope_capabilities = OA2_Scope_Helper::get_scope_capabilities( $this->authorized_token[ 'scope' ] ); 116 | 117 | // Only allow capabilities that are in scope and are allowed for user 118 | $allowed_capabilities = []; 119 | foreach($scope_capabilities as $capability) { 120 | $allowed_capabilities[$capability] = !empty( $allcaps[$capability] ); 121 | } 122 | 123 | return $allowed_capabilities; 124 | } 125 | 126 | return $allcaps; 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /oauth2-server.php: -------------------------------------------------------------------------------- 1 | 'GET', 63 | 'callback' => array( 'OA2_Authorize_Controller', 'validate' ), 64 | ) ); 65 | 66 | // Registers the token endpoint 67 | register_rest_route( 'oauth2/v1', '/token', array( 68 | 'methods' => 'POST', 69 | 'callback' => array( 'OA2_Token_Controller', 'validate' ), 70 | ) ); 71 | } 72 | 73 | /* Register routes to authentication on rest index 74 | * 75 | * @param object $response_object WP_REST_Response Object 76 | * @return object Filtered WP_REST_Response object 77 | */ 78 | static function add_routes_to_index( $response_object ) { 79 | if ( empty( $response_object->data[ 'authentication' ] ) ) { 80 | $response_object->data[ 'authentication' ] = array(); 81 | } 82 | 83 | $response_object->data[ 'authentication' ][ 'oauth2' ] = array( 84 | 'authorize' => get_rest_url(null, '/oauth2/v1/authorize' ), 85 | 'token' => get_rest_url(null, '/oauth2/v1/token' ), 86 | 'version' => '0.1', 87 | ); 88 | return $response_object; 89 | } 90 | 91 | /** 92 | * Register the CPTs needed for storage. 93 | */ 94 | static function register_storage() { 95 | register_post_type( 'json_consumer', array( 96 | 'labels' => array( 97 | 'name' => __( 'Consumers', 'wp_rest_oauth2' ), 98 | 'singular_name' => __( 'Consumer', 'wp_rest_oauth2' ), 99 | ), 100 | 'public' => false, 101 | 'hierarchical' => false, 102 | 'rewrite' => false, 103 | 'delete_with_user' => true, 104 | 'query_var' => false, 105 | ) ); 106 | 107 | register_post_type( 'oauth2_access_token', array( 108 | 'labels' => array( 109 | 'name' => __( 'Access tokens', 'wp_rest_oauth2' ), 110 | 'singular_name' => __( 'Access token', 'wp_rest_oauth2' ), 111 | ), 112 | 'public' => false, 113 | 'hierarchical' => false, 114 | 'rewrite' => false, 115 | 'delete_with_user' => true, 116 | 'query_var' => false, 117 | ) ); 118 | 119 | register_post_type( 'oauth2_refresh_token', array( 120 | 'labels' => array( 121 | 'name' => __( 'Refresh tokens', 'wp_rest_oauth2' ), 122 | 'singular_name' => __( 'Refresh token', 'wp_rest_oauth2' ), 123 | ), 124 | 'public' => false, 125 | 'hierarchical' => false, 126 | 'rewrite' => false, 127 | 'delete_with_user' => true, 128 | 'query_var' => false, 129 | ) ); 130 | } 131 | 132 | /** 133 | * Setup the authenticator. 134 | */ 135 | static function init_autheticator() { 136 | include_once( dirname( __FILE__ ) . '/lib/class-oa2-authenticator.php' ); 137 | 138 | self::$authenticator = new OA2_Authenticator(); 139 | } 140 | 141 | /** 142 | * Register the authorization page 143 | */ 144 | static function load_authorize_ui() { 145 | $authorize_ui = new OA2_UI(); 146 | $authorize_ui->register_hooks(); 147 | } 148 | 149 | /** 150 | * Load textdomain 151 | */ 152 | static function load_textdomain() { 153 | load_plugin_textdomain( 'wp_rest_oauth2', false, dirname( plugin_basename(__FILE__) ) . '/languages/' ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/storage/class-wp-rest-oauth-client.php: -------------------------------------------------------------------------------- 1 | post = $post; 27 | } 28 | 29 | /** 30 | * Getter. 31 | * 32 | * Passes through to post object. 33 | * 34 | * @param string $key Key to get. 35 | * @return mixed 36 | */ 37 | public function __get( $key ) { 38 | return $this->post->$key; 39 | } 40 | 41 | /** 42 | * Isset-er. 43 | * 44 | * Passes through to post object. 45 | * 46 | * @param string $key Property to check if set. 47 | * @return bool 48 | */ 49 | public function __isset( $key ) { 50 | return isset( $this->post->$key ); 51 | } 52 | 53 | /** 54 | * Update the client's post. 55 | * 56 | * @param array $params Parameters to update. 57 | * @return bool|WP_Error True on success, error object otherwise. 58 | */ 59 | public function update( $params ) { 60 | $data = array(); 61 | if ( isset( $params['name'] ) ) { 62 | $data['post_title'] = $params['name']; 63 | } 64 | if ( isset( $params['description'] ) ) { 65 | $data['post_content'] = $params['description']; 66 | } 67 | 68 | // Are we updating the post itself? 69 | if ( ! empty( $data ) ) { 70 | $data['ID'] = $this->post->ID; 71 | 72 | $result = wp_update_post( wp_slash( $data ), true ); 73 | if ( is_wp_error( $result ) ) { 74 | return $result; 75 | } 76 | 77 | // Reload the post property 78 | $this->post = get_post( $this->post->ID ); 79 | } 80 | 81 | // Are we updating any meta? 82 | if ( ! empty( $params['meta'] ) ) { 83 | $meta = $params['meta']; 84 | 85 | foreach ( $meta as $key => $value ) { 86 | $existing = get_post_meta( $this->post->ID, $key, true ); 87 | if ( $existing === $value ) { 88 | continue; 89 | } 90 | 91 | $did_update = update_post_meta( $this->post->ID, $key, wp_slash( $value ) ); 92 | if ( ! $did_update ) { 93 | return new WP_Error( 94 | 'rest_client_update_meta_failed', 95 | __( 'Could not update client metadata.', 'rest_oauth' ) 96 | ); 97 | } 98 | } 99 | } 100 | 101 | return true; 102 | } 103 | 104 | /** 105 | * Delete a client. 106 | * 107 | * @param string $type Client type. 108 | * @param int $id Client post ID. 109 | * @return bool True if delete, false otherwise. 110 | */ 111 | public function delete() { 112 | return (bool) wp_delete_post( $this->post->ID, true ); 113 | } 114 | 115 | /** 116 | * Get a client by ID. 117 | * 118 | * @param int $id Client post ID. 119 | * @return self|WP_Error 120 | */ 121 | public static function get( $id ) { 122 | $post = get_post( $id ); 123 | if ( empty( $id ) || empty( $post ) || $post->post_type !== 'json_consumer' ) { 124 | return new WP_Error( 'rest_oauth2_invalid_id', __( 'Client ID is not valid.', 'wp_rest_oauth2' ), array( 'status' => 404 ) ); 125 | } 126 | 127 | $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); 128 | return new $class( $post ); 129 | } 130 | 131 | /** 132 | * Get a client by key. 133 | * 134 | * @param string $type Client type. 135 | * @param string $key Client key. 136 | * @return WP_Post|WP_Error 137 | */ 138 | public static function get_by_key( $key ) { 139 | $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); 140 | $type = call_user_func( array( $class, 'get_type' ) ); 141 | 142 | $query = new WP_Query(); 143 | $consumers = $query->query( array( 144 | 'post_type' => 'json_consumer', 145 | 'post_status' => 'any', 146 | 'meta_query' => array( 147 | array( 148 | 'key' => 'key', 149 | 'value' => $key, 150 | ), 151 | array( 152 | 'key' => 'type', 153 | 'value' => $type, 154 | ), 155 | ), 156 | ) ); 157 | 158 | if ( empty( $consumers ) || empty( $consumers[0] ) ) { 159 | return new WP_Error( 'json_consumer_notfound', __( 'Consumer Key is invalid', 'wp_rest_oauth2' ), array( 'status' => 401 ) ); 160 | } 161 | 162 | return $consumers[0]; 163 | } 164 | 165 | /** 166 | * Create a new client. 167 | * 168 | * @param string $type Client type. 169 | * @param array $params { 170 | * @type string $name Client name 171 | * @type string $description Client description 172 | * @type array $meta Metadata for the client (map of key => value) 173 | * } 174 | * @return WP_Post|WP_Error 175 | */ 176 | public static function create( $params ) { 177 | $default = array( 178 | 'name' => '', 179 | 'description' => '', 180 | 'meta' => array(), 181 | ); 182 | $params = wp_parse_args( $params, $default ); 183 | 184 | $data = array(); 185 | $data['post_title'] = $params['name']; 186 | $data['post_content'] = $params['description']; 187 | $data['post_type'] = 'json_consumer'; 188 | 189 | $ID = wp_insert_post( $data ); 190 | if ( is_wp_error( $ID ) ) { 191 | return $ID; 192 | } 193 | 194 | $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); 195 | $meta = $params['meta']; 196 | $meta['type'] = call_user_func( array( $class, 'get_type' ) ); 197 | 198 | // Allow types to add their own meta too 199 | $meta = $class::add_extra_meta( $meta, $params ); 200 | 201 | /** 202 | * Add extra meta to the consumer on creation. 203 | * 204 | * @param array $meta Metadata map of key => value 205 | * @param int $ID Post ID we created. 206 | * @param array $params Parameters passed to create. 207 | */ 208 | $meta = apply_filters( 'wp_rest_oauth2_json_consumer_meta', $meta, $ID, $params ); 209 | 210 | foreach ( $meta as $key => $value ) { 211 | update_post_meta( $ID, $key, $value ); 212 | } 213 | 214 | $post = get_post( $ID ); 215 | return new $class( $post ); 216 | } 217 | 218 | /** 219 | * Add extra meta to a post. 220 | * 221 | * If you'd like to add extra meta on client creation, add it here. This 222 | * works the same as a filter; make sure you return the original array! 223 | * 224 | * @param array $meta Metadata for the post. 225 | * @param array $params Parameters used to create the post. 226 | * @return array Metadata to actually save. 227 | */ 228 | protected static function add_extra_meta( $meta, $params ) { 229 | return $meta; 230 | } 231 | 232 | /** 233 | * Shim for get_called_class() for PHP 5.2 234 | * 235 | * @return string Class name. 236 | */ 237 | protected static function get_called_class() { 238 | // PHP 5.2 only 239 | $backtrace = debug_backtrace(); 240 | // [0] WP_REST_Client::get_called_class() 241 | // [1] WP_REST_Client::function() 242 | if ( 'call_user_func' === $backtrace[2]['function'] ) { 243 | return $backtrace[2]['args'][0][0]; 244 | } 245 | return $backtrace[2]['class']; 246 | } 247 | } -------------------------------------------------------------------------------- /lib/class-oa2-token-controller.php: -------------------------------------------------------------------------------- 1 | get_body_params(); 18 | foreach( $required_params as $required_param ) { 19 | if ( empty( $request_body_params[ $required_param ] ) ) { 20 | $required_missing = true; 21 | } 22 | } 23 | 24 | if ( $required_missing ) { 25 | $error = OA2_Error_Helper::get_error( 'invalid_request' ); 26 | 27 | return new WP_REST_Response( $error ); 28 | } 29 | 30 | // Check that client credentials are valid 31 | // We may be able to move this up in the first check as well 32 | if ( !OA2_Storage_Controller::authenticateClient( $request_body_params[ 'client_id' ], $request_body_params[ 'client_secret' ] ) ) { 33 | $error = OA2_Error_Helper::get_error( 'invalid_request' ); 34 | 35 | return new WP_REST_Response( $error ); 36 | } 37 | 38 | $supported_grant_types = apply_filters( 'wp_rest_oauth2_grant_types', array( 'authorization_code', 'refresh_token' ) ); 39 | 40 | if ( !in_array( $request_body_params[ 'grant_type' ], $supported_grant_types ) ) { 41 | $error = OA2_Error_Helper::get_error( 'unsupported_grant_type' ); 42 | return new WP_REST_Response( $error ); 43 | } 44 | 45 | $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); 46 | 47 | switch( $request_body_params[ 'grant_type' ] ) { 48 | case 'authorization_code': 49 | return $class::handleAuthorizationCode( $request ); 50 | case 'refresh_token': 51 | return $class::handleRefreshToken( $request ); 52 | default: 53 | return apply_filters( 'wp_rest_oauth2_grant_type_' . $request_body_params[ 'grant_type' ], null, $request ); 54 | } 55 | } 56 | 57 | /** 58 | * Handle grant_type 'authorization_code' 59 | * 60 | * @param WP_REST_Request $request 61 | * @return \WP_REST_Response 62 | */ 63 | static function handleAuthorizationCode ( WP_REST_Request $request ) { 64 | $request_body_params = $request->get_body_params(); 65 | 66 | // Check that redirect_uri and code is set 67 | if ( empty( $request_body_params[ 'redirect_uri' ]) || empty( $request_body_params[ 'code' ] ) ) { 68 | $error = OA2_Error_Helper::get_error( 'invalid_request' ); 69 | return new WP_REST_Response( $error ); 70 | } 71 | 72 | $request_code = $request_body_params[ 'code' ]; 73 | $code = OA2_Authorization_Code::get_authorization_code( $request_code ); 74 | 75 | // Authorization code MUST exist 76 | if ( empty( $code ) ) { 77 | // Ensure that all tokens created with the code are removed. 78 | $access_tokens = OA2_Access_Token::revoke_tokens_by_auth_code( $request_code ); 79 | $refresh_tokens = OA2_Refresh_Token::revoke_tokens_by_auth_code( $request_code ); 80 | 81 | // Log the incident 82 | error_log( 83 | 'Authorization code not found. Possibly a code replay.' . 84 | ' Code: ' . $request_code . 85 | ' Revoked access tokens: ' . print_r( $access_tokens, true ) . 86 | ' Revoked refresh tokens: ' . print_r( $refresh_tokens, true ) 87 | ); 88 | 89 | $error = OA2_Error_Helper::get_error( 'invalid_request' ); 90 | return new WP_REST_Response( $error ); 91 | } 92 | 93 | // Authorization code redirect URI and client MUST match, and the code should not have expired 94 | $is_valid_redirect_uri = !empty( $code[ 'redirect_uri' ] ) && $code[ 'redirect_uri' ] === $request_body_params[ 'redirect_uri' ]; 95 | $is_valid_client = $code[ 'client_id' ] === $request_body_params[ 'client_id' ]; 96 | $code_has_expired = $code[ 'expires' ] < time(); 97 | if ( !$is_valid_redirect_uri || !$is_valid_client || $code_has_expired ) { 98 | $error = OA2_Error_Helper::get_error( 'invalid_request' ); 99 | 100 | return new WP_REST_Response( $error ); 101 | } 102 | 103 | // Codes are single use, remove it 104 | if ( !OA2_Authorization_Code::revoke_code( $request_code ) ) { 105 | $error = OA2_Error_Helper::get_error( 'server_error' ); 106 | 107 | return new WP_REST_Response( $error ); 108 | } 109 | 110 | // Store authorization code hash to access token (for possible revocation). 111 | $extra_metas = array( 112 | 'authorization_code' => $code[ 'hash' ] 113 | ); 114 | $access_token = OA2_Access_Token::generate_token( $code[ 'client_id' ], $code[ 'user_id' ], time() + MONTH_IN_SECONDS, $code[ 'scope' ], $extra_metas ); 115 | 116 | // Store authorization code and access token hash to refresh token (for possible revocation). 117 | $extra_metas[ 'access_token' ] = $access_token[ 'hash' ]; 118 | $refresh_token = OA2_Refresh_Token::generate_token( $code[ 'client_id' ], $code[ 'user_id' ], time() + YEAR_IN_SECONDS, $code[ 'scope' ], $extra_metas ); 119 | 120 | $data = array( 121 | "access_token" => $access_token[ 'token' ], 122 | "expires_in" => MONTH_IN_SECONDS, 123 | "token_type" => "Bearer", 124 | "refresh_token" => $refresh_token[ 'token' ] 125 | ); 126 | 127 | return new WP_REST_Response( $data ); 128 | } 129 | 130 | /** 131 | * Handle grant_type 'refresh_token' 132 | * 133 | * @param WP_REST_Request $request 134 | * @return \WP_REST_Response 135 | */ 136 | static function handleRefreshToken ( WP_REST_Request $request ) { 137 | $request_body_params = $request->get_body_params(); 138 | $request_refresh_token = $request_body_params[ 'refresh_token' ]; 139 | $refresh_token = OA2_Refresh_Token::get_token( $request_refresh_token ); 140 | 141 | // Refresh token MUST exist 142 | if ( is_wp_error( $refresh_token ) ) { 143 | // Ensure that all tokens created with the code are removed. 144 | $access_tokens = OA2_Access_Token::revoke_tokens_by_refresh_token( $request_refresh_token ); 145 | $refresh_tokens = OA2_Refresh_Token::revoke_tokens_by_refresh_token( $request_refresh_token ); 146 | 147 | // Log the incident 148 | error_log( 149 | 'Refresh token not found. Possibly a refresh token replay.' . 150 | ' Refresh token: ' . $request_refresh_token . 151 | ' Revoked access tokens: ' . print_r( $access_tokens, true ) . 152 | ' Revoked refresh tokens: ' . print_r( $refresh_tokens, true ) 153 | ); 154 | 155 | $error = OA2_Error_Helper::get_error( 'invalid_grant' ); 156 | return new WP_REST_Response( $error ); 157 | } 158 | 159 | // Refresh token client MUST match, and the code should not have expired 160 | $is_valid_client = $refresh_token[ 'client_id' ] === $request_body_params[ 'client_id' ]; 161 | if ( !$is_valid_client ) { 162 | $error = OA2_Error_Helper::get_error( 'invalid_request' ); 163 | 164 | return new WP_REST_Response( $error ); 165 | } 166 | 167 | $code_has_expired = $refresh_token[ 'expires' ] < time(); 168 | if ( $code_has_expired ) { 169 | $error = OA2_Error_Helper::get_error( 'invalid_grant' ); 170 | 171 | return new WP_REST_Response( $error ); 172 | } 173 | 174 | // Requested scope should be a subset of the Refresh Token scope. 175 | $scope = $refresh_token[ 'scope' ]; 176 | if ( isset( $request_body_params[ 'scope' ] ) ) { 177 | $scope = $request_body_params[ 'scope' ]; 178 | if ( $refresh_token[ 'scope' ] !== OA2_Scope_Helper::get_all_caps_scope() ) { 179 | if ( $request_body_params[ 'scope' ] === OA2_Scope_Helper::get_all_caps_scope() ) { 180 | $scope_allowed = false; 181 | } else { 182 | $scope_allowed = true; 183 | $refresh_token_scopes = OA2_Scope_Helper::get_scope_capabilities( $refresh_token[ 'scope' ] ); 184 | $requested_scopes = OA2_Scope_Helper::get_scope_capabilities( $request_body_params[ 'scope' ] ); 185 | 186 | foreach( $requested_scopes as $requested_scope ) { 187 | if ( !in_array( $requested_scope, $refresh_token_scopes ) ) { 188 | $scope_allowed = false; 189 | break; 190 | } 191 | } 192 | } 193 | 194 | if ( !$scope_allowed ) { 195 | $error = OA2_Error_Helper::get_error( 'invalid_scope' ); 196 | 197 | return new WP_REST_Response( $error ); 198 | } 199 | } 200 | } 201 | 202 | // Refresh tokens are single use, remove the token 203 | if ( !OA2_Refresh_Token::revoke_token( $request_refresh_token ) ) { 204 | $error = OA2_Error_Helper::get_error( 'server_error' ); 205 | 206 | return new WP_REST_Response( $error ); 207 | } 208 | 209 | // Store refresh token to access token (for possible revocation). 210 | $extra_metas = array( 211 | 'refresh_token' => $refresh_token[ 'hash' ] 212 | ); 213 | $access_token = OA2_Access_Token::generate_token( $refresh_token[ 'client_id' ], $refresh_token[ 'user_id' ], time() + MONTH_IN_SECONDS, $scope, $extra_metas ); 214 | 215 | // Store refresh token and access token hash to refresh token (for possible revocation). 216 | $extra_metas[ 'access_token' ] = $access_token[ 'hash' ]; 217 | $new_refresh_token = OA2_Refresh_Token::generate_token( $refresh_token[ 'client_id' ], $refresh_token[ 'user_id' ], time() + YEAR_IN_SECONDS, $scope, $extra_metas ); 218 | 219 | $data = array( 220 | "access_token" => $access_token[ 'token' ], 221 | "expires_in" => MONTH_IN_SECONDS, 222 | "token_type" => "Bearer", 223 | "refresh_token" => $new_refresh_token[ 'token' ] 224 | ); 225 | 226 | return new WP_REST_Response( $data ); 227 | } 228 | 229 | } 230 | -------------------------------------------------------------------------------- /lib/storage/class-oa2-token.php: -------------------------------------------------------------------------------- 1 | 401 ) ); 34 | } 35 | 36 | $token = $class::get_token_by_id( $post_id ); 37 | 38 | return $token; 39 | } 40 | 41 | /** 42 | * Retrieve token based on post ID 43 | * 44 | * @param type $post_id 45 | * @return array|\WP_Error Key-value array on success, WP_Error otherwise. 46 | */ 47 | public static function get_token_by_id( $post_id ) { 48 | $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); 49 | $token_type = $class::get_type(); 50 | 51 | $post = get_post( $post_id ); 52 | 53 | if ( !$class::post_is_token( $post ) ) { 54 | return new WP_Error( 'rest_oauth2_token_id_not_valid', __( 'Token with given post ID does not exist.', 'wp_rest_oauth2' ), array( 'status' => 401 ) ); 55 | } 56 | 57 | $expires = intval( get_post_meta( $post_id, 'expires', true ) ); // 0 on false 58 | 59 | if ( $expires > 0 && $expires < time() ) { 60 | $class::revoke_token_by_id( $post_id ); 61 | return new WP_Error( 'rest_oauth2_token_expired', __( 'Token has expired.', 'wp_rest_oauth2' ), array( 'status' => 401 ) ); 62 | } 63 | 64 | // Populate token data 65 | $token_data = array( 66 | 'hash' => $post->post_title, 67 | 'type' => $token_type, 68 | 'post_id' => $post->ID, 69 | 'client_id' => get_post_meta( $post_id, 'client_id', true ), 70 | 'user_id' => $post->post_author, 71 | 'expires' => $expires, 72 | 'scope' => get_post_meta( $post_id, 'scope', true ) 73 | ); 74 | 75 | return $token_data; 76 | } 77 | 78 | /** 79 | * Generate a new token 80 | * 81 | * @param int $client_id 82 | * @param int $user_id 83 | * @param int $expires 84 | * @param string $scope Comma separated list of allowed capabilities 85 | * @param array $extra_metas Key-value array of extra metas to add. 86 | * @return WP_Error|array OAuth token data on success, error otherwise 87 | */ 88 | public static function generate_token( $client_id, $user_id, $expires = 0, $scope = '*', $extra_metas = array() ) { 89 | $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); 90 | $token_type = $class::get_type(); 91 | 92 | // Issue access token 93 | $token = apply_filters( 'wp_rest_oauth2_' . $token_type . '_token', wp_generate_password( self::TOKEN_KEY_LENGTH, false ) ); 94 | 95 | // Check that client exists 96 | $consumer = OA2_Client::get_by_client_id( $client_id ); 97 | if ( is_wp_error( $consumer ) ) { 98 | return $consumer; 99 | } 100 | 101 | // Setup data for filtering 102 | $unfiltered_data = array( 103 | 'token' => $token, 104 | 'hash' => $class::hash_token( $token ), 105 | 'type' => $token_type, 106 | 'user_id' => intval( $user_id ), 107 | 'client_id' => get_post_meta( $consumer->ID, 'client_id', true ), 108 | 'expires' => intval( $expires ), 109 | 'scope' => $scope 110 | ); 111 | $data = apply_filters( 'wp_rest_oauth2_' . $token_type . '_token_data', $unfiltered_data ); 112 | 113 | // Insert token to DB 114 | $new_id = wp_insert_post( array( 115 | 'post_title' => $data[ 'hash' ], 116 | 'post_type' => 'oauth2_' . $data[ 'type' ] . '_token', 117 | 'post_author' => $data[ 'user_id' ] 118 | ) ); 119 | 120 | if ( empty( $new_id ) || is_wp_error( $new_id ) ) { 121 | return new WP_Error( 'rest_oauth2_token_save_failed', __( 'Could not save new token.', 'wp_rest_oauth2' ), array( 'status' => 401 ) ); 122 | } 123 | 124 | // Add metas 125 | $metas = array( 'client_id', 'expires', 'scope' ); 126 | foreach ( $metas as $meta ) { 127 | add_post_meta( $new_id, $meta, $data[ $meta ] ); 128 | } 129 | $sanitize_extra_metas = $class::sanitize_extra_meta( $extra_metas, $data ); 130 | foreach ( $sanitize_extra_metas as $key => $value ) { 131 | add_post_meta( $new_id, $key, $value ); 132 | } 133 | 134 | return $data; 135 | } 136 | 137 | /** 138 | * Revoke a token 139 | * 140 | * @param string $oauth_token Access token 141 | * @return array|false|WP_Post|WP_Error WP_Error or False on failure. 142 | */ 143 | public static function revoke_token( $oauth_token ) { 144 | $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); 145 | $token_title = $class::hash_token( $oauth_token ); 146 | $post_id = $class::get_post_id_by_title( $token_title ); 147 | 148 | if ( empty( $post_id ) ) { 149 | return new WP_Error( 'rest_oauth2_token_not_exists', __( 'Token does not exist.', 'wp_rest_oauth2' ), array( 'status' => 401 ) ); 150 | } 151 | 152 | $result = $class::revoke_token_by_id( $post_id ); 153 | 154 | return $result; 155 | } 156 | 157 | /** 158 | * Revokes a token by ID. 159 | * 160 | * @param type $post_id 161 | * @return array|false|WP_Post|WP_Error WP_Error or False on failure. 162 | */ 163 | public static function revoke_token_by_id( $post_id ) { 164 | $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); 165 | 166 | $post = get_post( $post_id ); 167 | 168 | if ( !$class::post_is_token( $post ) ) { 169 | return new WP_Error( 'rest_oauth2_token_id_not_valid', __( 'Token with given post ID does not exist.', 'wp_rest_oauth2' ), array( 'status' => 401 ) ); 170 | } 171 | 172 | return wp_delete_post( $post_id, true ); 173 | } 174 | 175 | /** 176 | * Revokes tokens created with the given authorization code 177 | * 178 | * @param string $auth_code Authorization code. 179 | * @return array Array of the deleted tokens. 180 | */ 181 | public static function revoke_tokens_by_auth_code( $auth_code ) { 182 | $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); 183 | $token_type = $class::get_type(); 184 | 185 | $args = array( 186 | 'post_type' => 'oauth2_' . $token_type . '_token', 187 | 'post_status' => 'any', 188 | 'meta_query' => array( 189 | array( 190 | 'key' => 'authorization_code', 191 | 'value' => OA2_Authorization_Code::hash_code( $auth_code ), 192 | ) 193 | ) 194 | ); 195 | 196 | return $class::revoke_tokens_by_args( $args ); 197 | } 198 | 199 | /** 200 | * Revokes tokens created with the given authorization code 201 | * 202 | * @param string $refresh_token Refresh token 203 | * @return array Array of the deleted tokens. 204 | */ 205 | public static function revoke_tokens_by_refresh_token( $refresh_token ) { 206 | $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); 207 | $token_type = $class::get_type(); 208 | 209 | $args = array( 210 | 'post_type' => 'oauth2_' . $token_type . '_token', 211 | 'post_status' => 'any', 212 | 'meta_query' => array( 213 | array( 214 | 'key' => 'refresh_token', 215 | 'value' => OA2_Refresh_Token::hash_token( $refresh_token ), 216 | ) 217 | ) 218 | ); 219 | 220 | return $class::revoke_tokens_by_args( $args ); 221 | } 222 | 223 | /** 224 | * Revoke posts matching the given query args. 225 | * 226 | * @param array $args 227 | * @return array array Array of the deleted tokens. 228 | */ 229 | public static function revoke_tokens_by_args( $args ) { 230 | $query = new WP_query( $args ); 231 | $deleted_posts = array(); 232 | foreach( $query->posts as $post ) { 233 | $result = wp_delete_post( $post->ID, true ); 234 | if ( $result !== false ) { 235 | $deleted_posts[] = $result; 236 | } 237 | } 238 | return $deleted_posts; 239 | } 240 | 241 | /** 242 | * Check if given post object is a valid token. 243 | * 244 | * @param WP_Post $post 245 | * @return boolean 246 | */ 247 | public static function post_is_token( $post ) { 248 | $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); 249 | $token_type = $class::get_type(); 250 | 251 | if ( empty( $post ) || $post->post_type !== 'oauth2_' . $token_type . '_token' ) { 252 | return false; 253 | } 254 | return true; 255 | } 256 | 257 | /** 258 | * Return post id based on title 259 | * 260 | * @global type $wpdb 261 | * @param string $post_title 262 | * @return int|null Post ID, or null on failure 263 | */ 264 | public static function get_post_id_by_title( $post_title ) { 265 | global $wpdb; 266 | 267 | $query = $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_title = '%s' LIMIT 1", $post_title ); 268 | return $wpdb->get_var( $query ); 269 | } 270 | 271 | /** 272 | * Get tokens 273 | * 274 | * @param array $additional_args WP_Query args 275 | * @return \WP_Query 276 | */ 277 | public static function get_tokens_query( $additional_args = array() ) { 278 | $class = function_exists( 'get_called_class' ) ? get_called_class() : self::get_called_class(); 279 | $token_type = $class::get_type(); 280 | 281 | $defaults = array( 282 | 'post_type' => 'oauth2_' . $token_type . '_token', 283 | 'post_status' => 'any', 284 | 'posts_per_page' => -1 285 | ); 286 | 287 | $args = wp_parse_args( $additional_args, $defaults ); 288 | 289 | return new WP_Query( $args ); 290 | } 291 | 292 | /** 293 | * Get clients 294 | * 295 | * @param array $additional_args WP_Query args 296 | * @return array Array of WP_Posts 297 | */ 298 | public static function get_tokens( $additional_args = array() ) { 299 | $query = self::get_tokens_query( $additional_args ); 300 | 301 | return $query->posts; 302 | } 303 | 304 | /** 305 | * Sanitize extra meta to a post. 306 | * 307 | * If you'd like to add extra meta on token creation, add it here. This 308 | * works the same as a filter; make sure you return the original array! 309 | * 310 | * @param array $metas Metadata for the post as key-value array. 311 | * @param array $token Token data. 312 | * @return array Metadata to actually save. 313 | */ 314 | protected static function sanitize_extra_meta( $metas, $token ) { 315 | $sanitized_metas = array_map( 'sanitize_text_field', $metas ); 316 | return $sanitized_metas; 317 | } 318 | 319 | /** 320 | * Hash a token for DB 321 | * 322 | * @param string $oauth_token 323 | * @return string Hash 324 | */ 325 | protected static function hash_token( $oauth_token ) { 326 | $token_hash = wp_hash($oauth_token); 327 | return $token_hash; 328 | } 329 | 330 | } 331 | -------------------------------------------------------------------------------- /languages/wp_rest_oauth2.pot: -------------------------------------------------------------------------------- 1 | #, fuzzy 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: WP REST OAuth 2.0\n" 5 | "POT-Creation-Date: 2016-02-18 13:12+0200\n" 6 | "PO-Revision-Date: 2016-02-18 13:04+0200\n" 7 | "Last-Translator: A-P Koponen / Versi' . esc_html( $msg ) . '
' . __( 'You are not allowed to delete this application.', 'wp_rest_oauth2' ) . '
', 400 | 403 401 | ); 402 | } 403 | 404 | $client = OA2_Client::get( $id ); 405 | if ( is_wp_error( $client ) ) { 406 | wp_die( $client ); 407 | return; 408 | } 409 | 410 | if ( ! $client->delete() ) { 411 | $message = 'Invalid consumer ID'; 412 | wp_die( $message ); 413 | return; 414 | } 415 | 416 | wp_safe_redirect( self::get_url( 'deleted=1' ) ); 417 | exit; 418 | } 419 | 420 | public static function handle_regenerate() { 421 | if ( empty( $_GET['id'] ) ) { 422 | return; 423 | } 424 | 425 | $id = $_GET['id']; 426 | check_admin_referer( 'rest-oauth2-regenerate:' . $id ); 427 | 428 | if ( ! current_user_can( 'edit_post', $id ) ) { 429 | wp_die( 430 | '' . __( 'You are not allowed to edit this application.', 'wp_rest_oauth2' ) . '
', 432 | 403 433 | ); 434 | } 435 | 436 | $client = OA2_Client::get( $id ); 437 | $client->regenerate_secret(); 438 | 439 | wp_safe_redirect( self::get_url( array( 'action' => 'edit', 'id' => $id, 'did_action' => 'regenerate' ) ) ); 440 | exit; 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /languages/wp_rest_oauth2-fi.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: WP REST OAuth 2.0\n" 4 | "POT-Creation-Date: 2016-05-27 14:55+0300\n" 5 | "PO-Revision-Date: 2016-05-27 15:01+0300\n" 6 | "Last-Translator: A-P Koponen / Versi