Files
WooBC/woo-business-central/includes/class-wbc-oauth.php
Malin b64397dcd3 feat: WooCommerce Business Central integration plugin
Native PHP plugin (no Composer) that syncs:
- Product stock and pricing from BC to WooCommerce (scheduled cron)
- Orders from WooCommerce to BC (on payment received)
- Auto-creates customers in BC from WooCommerce billing data

Product matching: WooCommerce SKU → BC Item Number, fallback to GTIN (EAN).
OAuth2 client credentials auth with encrypted secret storage.
Admin settings page with connection test, manual sync, and log viewer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:59:53 +01:00

339 lines
9.7 KiB
PHP

<?php
/**
* OAuth2 Authentication for Business Central API
*
* @package WooBusinessCentral
*/
// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class WBC_OAuth
*
* Handles OAuth2 token management for Microsoft Business Central API.
*/
class WBC_OAuth {
/**
* Token endpoint URL template
*/
const TOKEN_ENDPOINT = 'https://login.microsoftonline.com/%s/oauth2/v2.0/token';
/**
* API scope
*/
const API_SCOPE = 'https://api.businesscentral.dynamics.com/.default';
/**
* Token transient name
*/
const TOKEN_TRANSIENT = 'wbc_access_token';
/**
* Token expiry buffer (refresh 5 minutes before expiry)
*/
const EXPIRY_BUFFER = 300;
/**
* Encryption key option name
*/
const ENCRYPTION_KEY_OPTION = 'wbc_encryption_key';
/**
* Get the encryption key
*
* @return string
*/
private static function get_encryption_key() {
$key = get_option( self::ENCRYPTION_KEY_OPTION );
if ( empty( $key ) ) {
// Generate a new key if one doesn't exist
$key = wp_generate_password( 32, true, true );
update_option( self::ENCRYPTION_KEY_OPTION, $key );
}
return $key;
}
/**
* Encrypt a value
*
* @param string $value The value to encrypt.
* @return string Encrypted value.
*/
public static function encrypt( $value ) {
if ( empty( $value ) ) {
return '';
}
$key = self::get_encryption_key();
// Use WordPress salt for additional entropy
$salt = wp_salt( 'auth' );
$key = hash( 'sha256', $key . $salt, true );
// Generate random IV
$iv_length = openssl_cipher_iv_length( 'aes-256-cbc' );
$iv = openssl_random_pseudo_bytes( $iv_length );
// Encrypt (OPENSSL_RAW_DATA ensures raw binary output)
$encrypted = openssl_encrypt( $value, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
if ( $encrypted === false ) {
return '';
}
// Combine IV and encrypted data, then base64 encode
return base64_encode( $iv . $encrypted );
}
/**
* Decrypt a value
*
* @param string $encrypted The encrypted value.
* @return string Decrypted value.
*/
public static function decrypt( $encrypted ) {
if ( empty( $encrypted ) ) {
return '';
}
$key = self::get_encryption_key();
// Use WordPress salt for additional entropy
$salt = wp_salt( 'auth' );
$key = hash( 'sha256', $key . $salt, true );
// Decode
$data = base64_decode( $encrypted );
if ( $data === false ) {
return '';
}
// Extract IV and encrypted data
$iv_length = openssl_cipher_iv_length( 'aes-256-cbc' );
$iv = substr( $data, 0, $iv_length );
$encrypted_data = substr( $data, $iv_length );
// Decrypt (OPENSSL_RAW_DATA matches the encrypt flag)
$decrypted = openssl_decrypt( $encrypted_data, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
return $decrypted !== false ? $decrypted : '';
}
/**
* Get stored tenant ID
*
* @return string
*/
public static function get_tenant_id() {
return get_option( 'wbc_tenant_id', '' );
}
/**
* Get stored client ID
*
* @return string
*/
public static function get_client_id() {
return get_option( 'wbc_client_id', '' );
}
/**
* Get stored client secret (decrypted)
*
* @return string
*/
public static function get_client_secret() {
$encrypted = get_option( 'wbc_client_secret', '' );
return self::decrypt( $encrypted );
}
/**
* Save client secret (encrypted)
*
* @param string $secret The client secret to save.
*/
public static function save_client_secret( $secret ) {
$encrypted = self::encrypt( $secret );
update_option( 'wbc_client_secret', $encrypted );
}
/**
* Get environment (production/sandbox)
*
* @return string
*/
public static function get_environment() {
return get_option( 'wbc_environment', 'production' );
}
/**
* Get company ID
*
* @return string
*/
public static function get_company_id() {
return get_option( 'wbc_company_id', '' );
}
/**
* Check if credentials are configured
*
* @return bool
*/
public static function is_configured() {
return ! empty( self::get_tenant_id() )
&& ! empty( self::get_client_id() )
&& ! empty( self::get_client_secret() )
&& ! empty( self::get_company_id() );
}
/**
* Get access token
*
* @param bool $force_refresh Force refresh even if cached token exists.
* @return string|WP_Error Access token or error.
*/
public static function get_access_token( $force_refresh = false ) {
// Check for cached token
if ( ! $force_refresh ) {
$cached_token = get_transient( self::TOKEN_TRANSIENT );
if ( $cached_token !== false ) {
return $cached_token;
}
}
// Request new token
return self::request_token();
}
/**
* Request a new access token from Microsoft
*
* @return string|WP_Error Access token or error.
*/
private static function request_token() {
$tenant_id = self::get_tenant_id();
$client_id = self::get_client_id();
$client_secret = self::get_client_secret();
// Validate credentials
if ( empty( $tenant_id ) || empty( $client_id ) || empty( $client_secret ) ) {
$error = new WP_Error( 'wbc_missing_credentials', __( 'Business Central API credentials are not configured.', 'woo-business-central' ) );
WBC_Logger::error( 'OAuth', 'Missing API credentials' );
return $error;
}
// Build token endpoint URL
$token_url = sprintf( self::TOKEN_ENDPOINT, $tenant_id );
// Prepare request body
$body = array(
'grant_type' => 'client_credentials',
'client_id' => $client_id,
'client_secret' => $client_secret,
'scope' => self::API_SCOPE,
);
WBC_Logger::debug( 'OAuth', 'Requesting new access token', array( 'tenant_id' => $tenant_id ) );
// Make the request using cURL
$ch = curl_init();
curl_setopt_array( $ch, array(
CURLOPT_URL => $token_url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query( $body ),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => array(
'Content-Type: application/x-www-form-urlencoded',
),
) );
$response = curl_exec( $ch );
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
$curl_error = curl_error( $ch );
curl_close( $ch );
// Check for cURL errors
if ( $response === false ) {
$error = new WP_Error( 'wbc_curl_error', sprintf( __( 'cURL error: %s', 'woo-business-central' ), $curl_error ) );
WBC_Logger::error( 'OAuth', 'cURL error during token request', array( 'error' => $curl_error ) );
return $error;
}
// Parse response
$data = json_decode( $response, true );
// Check for HTTP errors
if ( $http_code !== 200 ) {
$error_message = isset( $data['error_description'] ) ? $data['error_description'] : __( 'Unknown error', 'woo-business-central' );
$error = new WP_Error( 'wbc_token_error', $error_message, array( 'status' => $http_code ) );
WBC_Logger::error( 'OAuth', 'Token request failed', array(
'http_code' => $http_code,
'error' => $data['error'] ?? 'unknown',
'message' => $error_message,
) );
return $error;
}
// Extract token
if ( ! isset( $data['access_token'] ) ) {
$error = new WP_Error( 'wbc_invalid_response', __( 'Invalid token response from Microsoft.', 'woo-business-central' ) );
WBC_Logger::error( 'OAuth', 'Invalid token response', array( 'response' => $data ) );
return $error;
}
$access_token = $data['access_token'];
$expires_in = isset( $data['expires_in'] ) ? (int) $data['expires_in'] : 3600;
// Cache the token (with buffer for expiry)
$cache_duration = max( 60, $expires_in - self::EXPIRY_BUFFER );
set_transient( self::TOKEN_TRANSIENT, $access_token, $cache_duration );
WBC_Logger::info( 'OAuth', 'Successfully obtained access token', array(
'expires_in' => $expires_in,
'cached_for' => $cache_duration,
) );
return $access_token;
}
/**
* Clear cached token
*/
public static function clear_token_cache() {
delete_transient( self::TOKEN_TRANSIENT );
WBC_Logger::debug( 'OAuth', 'Token cache cleared' );
}
/**
* Test the connection to Business Central
*
* @return array|WP_Error Array with success status or WP_Error.
*/
public static function test_connection() {
// Clear any cached token
self::clear_token_cache();
// Try to get a new token
$token = self::get_access_token( true );
if ( is_wp_error( $token ) ) {
return $token;
}
return array(
'success' => true,
'message' => __( 'Successfully connected to Business Central.', 'woo-business-central' ),
);
}
}