Skip to content

Commit

Permalink
feat: add google token generation from service account
Browse files Browse the repository at this point in the history
  • Loading branch information
shekharnwagh committed Sep 4, 2024
1 parent ca2ee2a commit 49ee8e8
Show file tree
Hide file tree
Showing 4 changed files with 301 additions and 0 deletions.
149 changes: 149 additions & 0 deletions inc/config/auth/google-auth/google-auth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

namespace RemoteDataBlocks\Config\Auth;

use WP_Error;
use RemoteDataBlocks\Config\Auth\GoogleServiceAccountKey;

class GoogleAuth {
const TOKEN_EXPIRY_SECONDS = 3600; // 1 hour
/**
* The whitelisted scopes that are allowed to be used when generating a token.
* This avoids this being used to get tokens with broad scopes.
*/
const ALLOWED_SCOPES = [
'https://www.googleapis.com/auth/drive.readonly', // Drive Readonly
'https://www.googleapis.com/auth/spreadsheets.readonly', // Sheets Readonly
];

private static function get_allowed_scopes( array $scopes ): array {
return array_values( array_intersect( $scopes, self::ALLOWED_SCOPES ) );
}

/**
* Generate a token from a service account key.
*
* @param array $raw_service_account_key The service account key.
* @param array $scopes The scopes to generate the token for.
* @return WP_Error|string The token or an error.
*/
public static function generate_token_from_service_account_key(
array $raw_service_account_key,
array $scopes
): WP_Error|string {
$filtered_scopes = self::get_allowed_scopes( $scopes );

if ( empty( $filtered_scopes ) ) {
return new WP_Error(
'google_auth_error',
__( 'No valid scopes provided', 'remote-data-blocks' )
);
}

$scope = implode( ' ', $filtered_scopes );

$service_account_key = GoogleServiceAccountKey::from_array( $raw_service_account_key );
if ( is_wp_error( $service_account_key ) ) {
return $service_account_key;
}

$jwt = self::generate_jwt( $service_account_key, $scope );
$token_uri = $service_account_key->token_uri;

return self::get_token_using_jwt( $jwt, $token_uri );
}

private static function get_token_using_jwt( string $jwt, string $token_uri ): WP_Error|string {
$response = wp_remote_post(
$token_uri,
[
'body' => [
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwt,
],
]
);

if ( is_wp_error( $response ) ) {
return new WP_Error(
'google_auth_error',
__( 'Failed to retrieve access token', 'remote-data-blocks' )
);
}

$response_body = wp_remote_retrieve_body( $response );
$response_data = json_decode( $response_body, true );

if ( ! isset( $response_data['access_token'] ) ) {
return new WP_Error(
'google_auth_error',
__( 'Invalid response from Google Auth', 'remote-data-blocks' )
);
}

return $response_data['access_token'];
}

private static function generate_jwt(
GoogleServiceAccountKey $service_account_key,
string $scope
): string {
$header = self::generate_jwt_header();
$payload = self::generate_jwt_payload(
$service_account_key->client_email,
$service_account_key->token_uri,
$scope
);

$base64_url_header = base64_encode( wp_json_encode( $header ) );
$base64_url_payload = base64_encode( wp_json_encode( $payload ) );

$signature = self::generate_jwt_signature(
$base64_url_header,
$base64_url_payload,
$service_account_key->private_key
);
$base64_url_signature = base64_encode( $signature );

return $base64_url_header . '.' . $base64_url_payload . '.' . $base64_url_signature;
}

private static function generate_jwt_signature(
string $base64_url_header,
string $base64_url_payload,
string $private_key
): string {
$signature_input = $base64_url_header . '.' . $base64_url_payload;

openssl_sign( $signature_input, $signature, $private_key, 'sha256' );
return $signature;
}

private static function generate_jwt_header(): array {
$header = [
'alg' => 'RS256',
'typ' => 'JWT',
];

return $header;
}

private static function generate_jwt_payload(
string $client_email,
string $token_uri,
string $scope
): array {
$now = time();
$expiry = $now + self::TOKEN_EXPIRY_SECONDS;

$payload = [
'iss' => $client_email,
'scope' => $scope,
'aud' => $token_uri,
'exp' => $expiry,
'iat' => $now,
];

return $payload;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace RemoteDataBlocks\Config\Auth;

use WP_Error;

class GoogleServiceAccountKey {
public string $type;
public string $project_id;
public string $private_key_id;
public string $private_key;
public string $client_email;
public string $client_id;
public string $auth_uri;
public string $token_uri;
public string $auth_provider_x509_cert_url;
public string $client_x509_cert_url;
public string $universe_domain;

/**
* Validate the raw service account data.
*
* This function checks if the required fields are present in the raw service account data.
* It returns a WP_Error if any of the required fields are missing or invalid.
*
* Currently only validates the fields which are required to generate a JWT for getting Google
* Access Token.
*
* @param array $raw_service_account The raw service account data to validate.
* @return true|WP_Error Returns true if validation passes, or a WP_Error array if validation fails.
*/
public static function validate( array $raw_service_account_key ): WP_Error|true {
if ( ! isset( $raw_service_account_key['type'] ) ) {
return new WP_Error( 'missing_type', __( 'type is required', 'remote-data-blocks' ) );
}

if ( 'service_account' !== $raw_service_account_key['type'] ) {
return new WP_Error( 'invalid_type', __( 'type must be service_account', 'remote-data-blocks' ) );
}

if ( ! isset( $raw_service_account_key['project_id'] ) ) {
return new WP_Error( 'missing_project_id', __( 'project_id is required', 'remote-data-blocks' ) );
}

if ( ! isset( $raw_service_account_key['private_key'] ) ) {
return new WP_Error( 'missing_private_key', __( 'private_key is required', 'remote-data-blocks' ) );
}

if ( ! isset( $raw_service_account_key['client_email'] ) ) {
return new WP_Error( 'missing_client_email', __( 'client_email is required', 'remote-data-blocks' ) );
}

if ( ! isset( $raw_service_account_key['token_uri'] ) ) {
return new WP_Error( 'missing_token_uri', __( 'token_uri is required', 'remote-data-blocks' ) );
}

return true;
}

public function __construct( array $raw_service_account_key ) {
$this->type = $raw_service_account_key['type'];
$this->project_id = $raw_service_account_key['project_id'];
$this->private_key_id = $raw_service_account_key['private_key_id'];
$this->private_key = $raw_service_account_key['private_key'];
$this->client_email = $raw_service_account_key['client_email'];
$this->client_id = $raw_service_account_key['client_id'];
$this->auth_uri = $raw_service_account_key['auth_uri'];
$this->token_uri = $raw_service_account_key['token_uri'];
$this->auth_provider_x509_cert_url = $raw_service_account_key['auth_provider_x509_cert_url'];
$this->client_x509_cert_url = $raw_service_account_key['client_x509_cert_url'];
$this->universe_domain = $raw_service_account_key['universe_domain'];
}

public static function from_array( array $raw_service_account_key ): WP_Error|GoogleServiceAccountKey {
$validation_error = self::validate( $raw_service_account_key );
if ( is_wp_error( $validation_error ) ) {
return new WP_Error(
'invalid_service_account_key',
__( 'Invalid service account key:', 'remote-data-blocks' ) . ' ' . $validation_error->get_error_message(),
$validation_error
);
}

return new self( $raw_service_account_key );
}
}
4 changes: 4 additions & 0 deletions inc/plugin-settings/plugin-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace RemoteDataBlocks;

use RemoteDataBlocks\REST\DatasourceController;
use RemoteDataBlocks\REST\AuthController;
use function wp_get_environment_type;
use function wp_is_development_mode;

Expand Down Expand Up @@ -37,6 +38,9 @@ public static function settings_page_content() {
public static function init_rest_routes() {
$controller = new DatasourceController();
$controller->register_routes();

$auth_controller = new AuthController();
$auth_controller->register_routes();
}

public static function enqueue_settings_assets( $admin_page ) {
Expand Down
62 changes: 62 additions & 0 deletions inc/rest/auth-controller/auth-controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace RemoteDataBlocks\REST;

use WP_REST_Controller;
use WP_REST_Request;
use RemoteDataBlocks\Config\Auth\GoogleAuth;

defined( 'ABSPATH' ) || exit();
defined( 'ABSPATH' ) || exit();

class AuthController extends WP_REST_Controller {
public function __construct() {
$this->namespace = REMOTE_DATA_BLOCKS__REST_NAMESPACE;
$this->rest_base = 'auth';
}

public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/google/token',
[
'methods' => 'POST',
'callback' => [ $this, 'get_google_auth_token' ],
'permission_callback' => [ $this, 'get_google_auth_token_permissions_check' ],
]
);
}

public function get_google_auth_token( WP_REST_Request $request ) {
$params = $request->get_json_params();
$credentials = $params['credentials'] ?? null;
$scopes = $params['scopes'] ?? [];
$type = $params['type'] ?? null;

if ( ! $credentials || ! $type || ! $scopes ) {
return new \WP_Error(
'missing_parameters',
__( 'Credentials, type and scopes are required.', 'remote-data-blocks' ),
array( 'status' => 400 )
);
}

if ( 'service_account' === $type ) {
$token = GoogleAuth::generate_token_from_service_account_key( $credentials, $scopes );
if ( is_wp_error( $token ) ) {
return rest_ensure_response( $token );
}
return rest_ensure_response( [ 'token' => $token ] );
}

return new \WP_Error(
'invalid_type',
__( 'Invalid type. Supported types: service_account', 'remote-data-blocks' ),
array( 'status' => 400 )
);
}

public function get_google_auth_token_permissions_check( $request ) {
return current_user_can( 'manage_options' );
}
}

0 comments on commit 49ee8e8

Please sign in to comment.