-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add google token generation from service account
- Loading branch information
1 parent
ca2ee2a
commit 49ee8e8
Showing
4 changed files
with
301 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
86 changes: 86 additions & 0 deletions
86
inc/config/auth/google-service-account-key/google-service-account-key.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' ); | ||
} | ||
} |