• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » How to securely integrate GitHub API repositories endpoints into WordPress custom plugins using REST API Controllers

How to securely integrate GitHub API repositories endpoints into WordPress custom plugins using REST API Controllers

Leveraging WordPress REST API Controllers for Secure GitHub Integration

Integrating external services like GitHub into a WordPress ecosystem demands a robust and secure approach, especially when dealing with sensitive repository data. This document outlines a production-ready strategy for securely exposing GitHub API endpoints within your custom WordPress plugins using the built-in REST API Controllers. This method ensures proper authentication, authorization, and data sanitization, adhering to WordPress best practices and mitigating common security vulnerabilities.

Setting Up the GitHub API Authentication

Before interacting with the GitHub API, secure authentication is paramount. We’ll utilize OAuth 2.0, specifically a Personal Access Token (PAT) for server-to-server communication. Storing this token securely within WordPress is critical. Avoid hardcoding it directly into your plugin files. Instead, leverage WordPress’s options API, ideally with encryption or by storing it in environment variables accessible to your server but not directly to the WordPress admin interface.

For demonstration purposes, we’ll assume the PAT is stored in a WordPress option named github_api_token. In a real-world enterprise scenario, consider using a dedicated secrets management system and fetching the token dynamically.

Creating a Custom REST API Controller

WordPress’s REST API Controllers provide a structured way to define API endpoints. We’ll create a class that extends WP_REST_Controller to manage our GitHub-related endpoints.

Create a new file, e.g., inc/class-github-api-controller.php, within your custom plugin’s directory structure.

<?php
/**
 * GitHub API Controller for custom WordPress plugin.
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

class My_GitHub_API_Controller extends WP_REST_Controller {

    /**
     * The base route for the controller.
     *
     * @var string
     */
    protected $namespace = 'my-plugin/v1';

    /**
     * The controller's base path.
     *
     * @var string
     */
    protected $rest_base = 'github';

    /**
     * Initialize the controller.
     */
    public function __construct() {
        $this->register_routes();
    }

    /**
     * Register the routes for the controller.
     */
    public function register_routes() {
        register_rest_route( $this->namespace, '/' . $this->rest_base . '/repositories', array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array( $this, 'get_repositories' ),
                'permission_callback' => array( $this, 'get_repositories_permissions_check' ),
                'args'                => $this->get_collection_params(),
            ),
        ) );

        register_rest_route( $this->namespace, '/' . $this->rest_base . '/repository/(?P<repo_id>[\w-]+)', array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array( $this, 'get_repository_details' ),
                'permission_callback' => array( $this, 'get_repository_details_permissions_check' ),
                'args'                => array(
                    'repo_id' => array(
                        'description' => __( 'Unique identifier for the repository.', 'my-plugin' ),
                        'type'        => 'string',
                        'required'    => true,
                    ),
                ),
            ),
        ) );
    }

    /**
     * Get repositories.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
     */
    public function get_repositories( WP_REST_Request $request ) {
        $github_token = get_option( 'github_api_token' );
        if ( empty( $github_token ) ) {
            return new WP_Error( 'github_token_missing', __( 'GitHub API token is not configured.', 'my-plugin' ), array( 'status' => 500 ) );
        }

        $username = $request->get_param( 'username' );
        if ( empty( $username ) ) {
            return new WP_Error( 'missing_parameter', __( 'Username parameter is required.', 'my-plugin' ), array( 'status' => 400 ) );
        }

        $github_api_url = sprintf( 'https://api.github.com/users/%s/repos', sanitize_text_field( $username ) );

        $response = wp_remote_get( $github_api_url, array(
            'headers' => array(
                'Authorization' => 'token ' . $github_token,
                'Accept'        => 'application/vnd.github.v3+json',
            ),
            'timeout' => 15, // Adjust timeout as needed
        ) );

        if ( is_wp_error( $response ) ) {
            return new WP_Error( 'github_api_error', $response->get_error_message(), array( 'status' => 500 ) );
        }

        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );

        if ( ! empty( $data['message'] ) ) {
            return new WP_Error( 'github_api_error', $data['message'], array( 'status' => $response['response']['code'] ) );
        }

        // Sanitize and format the data before returning
        $formatted_repos = array_map( function( $repo ) {
            return array(
                'id'          => $repo['id'],
                'name'        => sanitize_text_field( $repo['name'] ),
                'full_name'   => sanitize_text_field( $repo['full_name'] ),
                'html_url'    => esc_url_raw( $repo['html_url'] ),
                'description' => sanitize_textarea_field( $repo['description'] ),
                'stargazers_count' => intval( $repo['stargazers_count'] ),
                'forks_count' => intval( $repo['forks_count'] ),
            );
        }, $data );

        return new WP_REST_Response( $formatted_repos, 200 );
    }

    /**
     * Get repository details.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
     */
    public function get_repository_details( WP_REST_Request $request ) {
        $github_token = get_option( 'github_api_token' );
        if ( empty( $github_token ) ) {
            return new WP_Error( 'github_token_missing', __( 'GitHub API token is not configured.', 'my-plugin' ), array( 'status' => 500 ) );
        }

        $repo_id = $request->get_param( 'repo_id' );
        // Basic validation, GitHub repo IDs are typically numeric, but names can be alphanumeric.
        // For simplicity, we'll assume repo_id is the repository name here for the API call.
        // A more robust solution might involve a lookup or using the full name.
        $repo_name = sanitize_text_field( $repo_id );

        if ( empty( $repo_name ) ) {
            return new WP_Error( 'missing_parameter', __( 'Repository ID parameter is required.', 'my-plugin' ), array( 'status' => 400 ) );
        }

        // Assuming the repo_id is the repository name for the API endpoint.
        // For a more robust solution, you might need to fetch the owner from context or another parameter.
        // Example: https://api.github.com/repos/{owner}/{repo}
        // For this example, we'll assume a fixed owner or infer it if possible.
        // A common pattern is to use the full_name which includes owner.
        // Let's adjust to fetch a specific repo by full name if provided, or assume a default owner.
        // For simplicity, let's assume the request parameter is the full repo name like 'owner/repo-name'.
        $full_repo_name = sanitize_text_field( $repo_id );
        if ( strpos( $full_repo_name, '/' ) === false ) {
             return new WP_Error( 'invalid_parameter', __( 'Repository ID must be in the format "owner/repo-name".', 'my-plugin' ), array( 'status' => 400 ) );
        }

        $github_api_url = sprintf( 'https://api.github.com/repos/%s', $full_repo_name );

        $response = wp_remote_get( $github_api_url, array(
            'headers' => array(
                'Authorization' => 'token ' . $github_token,
                'Accept'        => 'application/vnd.github.v3+json',
            ),
            'timeout' => 15,
        ) );

        if ( is_wp_error( $response ) ) {
            return new WP_Error( 'github_api_error', $response->get_error_message(), array( 'status' => 500 ) );
        }

        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );

        if ( ! empty( $data['message'] ) ) {
            return new WP_Error( 'github_api_error', $data['message'], array( 'status' => $response['response']['code'] ) );
        }

        // Sanitize and format the data
        $formatted_repo = array(
            'id'               => $data['id'],
            'name'             => sanitize_text_field( $data['name'] ),
            'full_name'        => sanitize_text_field( $data['full_name'] ),
            'html_url'         => esc_url_raw( $data['html_url'] ),
            'description'      => sanitize_textarea_field( $data['description'] ),
            'stargazers_count' => intval( $data['stargazers_count'] ),
            'forks_count'      => intval( $data['forks_count'] ),
            'created_at'       => sanitize_text_field( $data['created_at'] ),
            'updated_at'       => sanitize_text_field( $data['updated_at'] ),
            'owner'            => array(
                'login' => sanitize_text_field( $data['owner']['login'] ),
                'html_url' => esc_url_raw( $data['owner']['html_url'] ),
            ),
        );

        return new WP_REST_Response( $formatted_repo, 200 );
    }

    /**
     * Permission check for getting repositories.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return bool|WP_Error True if the request has permission, WP_Error object otherwise.
     */
    public function get_repositories_permissions_check( WP_REST_Request $request ) {
        // Implement your permission logic here.
        // For example, check if the user is logged in and has a specific capability.
        // Or, if this endpoint is public, return true.
        // For this example, we'll allow access if the user has 'read' capability.
        if ( current_user_can( 'read' ) ) {
            return true;
        }
        return new WP_Error( 'rest_forbidden', esc_html__( 'You do not have permission to access this resource.', 'my-plugin' ), array( 'status' => rest_authorization_required_code() ) );
    }

    /**
     * Permission check for getting repository details.
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return bool|WP_Error True if the request has permission, WP_Error object otherwise.
     */
    public function get_repository_details_permissions_check( WP_REST_Request $request ) {
        // Similar permission logic as above.
        if ( current_user_can( 'read' ) ) {
            return true;
        }
        return new WP_Error( 'rest_forbidden', esc_html__( 'You do not have permission to access this resource.', 'my-plugin' ), array( 'status' => rest_authorization_required_code() ) );
    }

    /**
     * Get the query params for collections.
     *
     * @return array
     */
    public function get_collection_params() {
        return array(
            'username' => array(
                'description' => __( 'GitHub username to fetch repositories for.', 'my-plugin' ),
                'type'        => 'string',
                'required'    => true,
                'sanitize_callback' => 'sanitize_text_field',
                'validate_callback' => array( $this, 'validate_username' ),
            ),
            'per_page' => array(
                'description' => __( 'Maximum number of items to be returned.', 'my-plugin' ),
                'type'        => 'integer',
                'default'     => 10,
                'minimum'     => 1,
                'maximum'     => 100, // GitHub API limits
                'sanitize_callback' => 'absint',
                'validate_callback' => 'rest_validate_request_arg',
            ),
            'page' => array(
                'description' => __( 'Current page of the collection.', 'my-plugin' ),
                'type'        => 'integer',
                'default'     => 1,
                'minimum'     => 1,
                'sanitize_callback' => 'absint',
                'validate_callback' => 'rest_validate_request_arg',
            ),
        );
    }

    /**
     * Validate GitHub username.
     *
     * @param mixed $param The parameter value.
     * @param WP_REST_Request $request The request object.
     * @param string $param_name The parameter name.
     * @return WP_Error|bool
     */
    public function validate_username( $param, $request, $param_name ) {
        if ( ! preg_match( '/^[a-zA-Z0-9-]+$/', $param ) ) {
            return new WP_Error( 'rest_invalid_param', esc_html__( 'Invalid username format.', 'my-plugin' ), array( 'status' => 400 ) );
        }
        return true;
    }
}

Registering the Controller

To make your controller active, you need to hook into the rest_api_init action. This is typically done in your plugin’s main file.

<?php
/**
 * Plugin Name: My GitHub Integration Plugin
 * Description: Integrates GitHub API endpoints into WordPress.
 * Version: 1.0.0
 * Author: Your Name
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

// Include the controller class.
require_once plugin_dir_path( __FILE__ ) . 'inc/class-github-api-controller.php';

/**
 * Register the REST API controller.
 */
function my_register_github_api_controller() {
    $controller = new My_GitHub_API_Controller();
}
add_action( 'rest_api_init', 'my_register_github_api_controller' );

// Add a placeholder for the GitHub token setting. In a real plugin, this would be in settings.
// For testing, you can manually add this option via WP-CLI or directly in the database.
// Example: wp option add github_api_token 'YOUR_GITHUB_PAT'
// Ensure this token has the 'repo' scope if you need to access private repos.
// For public repos, no specific scope might be needed beyond default.
?>

Securing the GitHub Personal Access Token (PAT)

Storing the GitHub PAT directly in the WordPress options table is a common practice, but it’s crucial to protect it. Here are several strategies:

  • Environment Variables: The most secure method. Store the PAT in an environment variable on your server (e.g., GITHUB_API_TOKEN). Your plugin can then read this variable using getenv('GITHUB_API_TOKEN'). This keeps secrets out of your codebase and database.
  • Encrypted Options: If environment variables are not feasible, consider encrypting the token before storing it in the WordPress option. You’d need a robust encryption/decryption mechanism.
  • WordPress Salts and Keys: WordPress uses salts and keys for cookie encryption. You could potentially leverage these for token encryption, but this requires careful implementation to avoid conflicts and ensure security.
  • External Secrets Management: For enterprise-level applications, integrate with dedicated secrets management tools like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault.

For the examples above, we’ve used get_option( 'github_api_token' ). If you opt for environment variables, modify the controller like this:

// In My_GitHub_API_Controller::get_repositories() and get_repository_details()
$github_token = getenv( 'GITHUB_API_TOKEN' );
if ( empty( $github_token ) ) {
    // Fallback to option if env var not set, or handle error
    $github_token = get_option( 'github_api_token' );
    if ( empty( $github_token ) ) {
        return new WP_Error( 'github_token_missing', __( 'GitHub API token is not configured.', 'my-plugin' ), array( 'status' => 500 ) );
    }
}

Implementing Permission Checks

The permission_callback is vital for controlling who can access your API endpoints. In the example, current_user_can( 'read' ) is used as a placeholder. You should tailor this to your specific needs:

  • Public Endpoints: If the data is public, you might simply return true.
  • Authenticated Users: Check if the user is logged in using is_user_logged_in().
  • Role-Based Access: Check for specific user roles or capabilities using current_user_can( 'your_custom_capability' ).
  • Nonce Verification: For POST/PUT/DELETE requests, always verify nonces to prevent CSRF attacks.

Data Sanitization and Validation

Never trust external input. All parameters received from the request and all data fetched from the GitHub API must be properly sanitized and validated before being used or returned.

In the controller:

  • Parameters: Use the 'sanitize_callback' and 'validate_callback' arguments in the $args array for your routes. We’ve added sanitize_text_field and a custom validate_username for the username parameter.
  • GitHub API Response: When processing the JSON response from GitHub, use WordPress sanitization functions like sanitize_text_field(), esc_url_raw(), sanitize_textarea_field(), and intval() on the data before returning it in the response. This prevents XSS vulnerabilities if the data is later rendered directly in HTML.

Making Requests to the GitHub API

The wp_remote_get() function is used for making HTTP requests to the GitHub API. Key considerations:

  • Headers: The Authorization header with your PAT and the Accept header are crucial for GitHub API v3.
  • Timeout: Set a reasonable timeout (e.g., 15 seconds) to prevent requests from hanging indefinitely.
  • Error Handling: Always check the return value of wp_remote_get() for WP_Error objects and handle API-specific error messages returned in the JSON response.
  • Rate Limiting: Be mindful of GitHub’s API rate limits. For authenticated requests, the limit is higher (typically 5000 requests per hour). Implement caching and consider using webhooks for real-time updates instead of frequent polling.

Testing the Endpoints

Once your plugin is active and the GitHub PAT is configured (e.g., via WP-CLI: wp option add github_api_token 'YOUR_GITHUB_PAT'), you can test the endpoints using tools like Postman, Insomnia, or curl.

# Get repositories for a user
curl -X GET "https://your-wordpress-site.com/wp-json/my-plugin/v1/github/repositories?username=octocat" \
     -H "Authorization: Bearer YOUR_WORDPRESS_API_TOKEN" # If authentication is enabled for WP REST API

# Get details for a specific repository (e.g., octocat/Spoon-Knife)
curl -X GET "https://your-wordpress-site.com/wp-json/my-plugin/v1/github/repository/octocat/Spoon-Knife" \
     -H "Authorization: Bearer YOUR_WORDPRESS_API_TOKEN" # If authentication is enabled for WP REST API

Note: If your WordPress REST API requires authentication (e.g., via an API key plugin or JWT), you’ll need to include that authentication token in your curl request’s Authorization header. If your endpoints are public, this might not be necessary.

Conclusion

By implementing custom REST API Controllers in WordPress, you can securely and efficiently integrate external services like GitHub. This approach leverages WordPress’s robust framework for routing, authentication, and data handling, ensuring a maintainable and secure solution for enterprise applications. Always prioritize secure storage of API credentials and rigorous data validation.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Step-by-Step Guide to building a custom multi-currency switcher block for Gutenberg using REST API custom routes
  • Advanced Diagnostics: Locating slow Dependency Injection Containers query bottlenecks in WooCommerce custom checkout pipelines
  • WordPress Development Recipe: Real-time custom event triggers using WebSockets and WP HTTP API
  • Reducing database query bloat in Understrap styling structures layouts using custom lazy loaders
  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in portfolio project grids

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (658)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (872)
  • PHP (5)
  • PHP Development (47)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (141)
  • WordPress Plugin Development (155)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Step-by-Step Guide to building a custom multi-currency switcher block for Gutenberg using REST API custom routes
  • Advanced Diagnostics: Locating slow Dependency Injection Containers query bottlenecks in WooCommerce custom checkout pipelines
  • WordPress Development Recipe: Real-time custom event triggers using WebSockets and WP HTTP API

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala