All checks were successful
Create Release / build (push) Successful in 6s
Store authenticated user ID on the auth object instance instead of trying to retrieve it from the REST server request. This was the root cause of all mobile API 500 errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
458 lines
14 KiB
PHP
458 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* Mobile App JWT Authentication Handler
|
|
*
|
|
* Handles JWT token generation, validation, and refresh for Android/iOS apps
|
|
*/
|
|
class TWP_Mobile_Auth {
|
|
|
|
private $secret_key;
|
|
private $token_expiry = 86400; // 24 hours in seconds
|
|
private $refresh_expiry = 2592000; // 30 days in seconds
|
|
private $current_user_id = null;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct() {
|
|
$this->secret_key = $this->get_secret_key();
|
|
}
|
|
|
|
/**
|
|
* Get or generate JWT secret key
|
|
*/
|
|
private function get_secret_key() {
|
|
$key = get_option('twp_mobile_jwt_secret');
|
|
|
|
if (empty($key)) {
|
|
// Generate a secure random key
|
|
$key = bin2hex(random_bytes(32));
|
|
update_option('twp_mobile_jwt_secret', $key);
|
|
}
|
|
|
|
return $key;
|
|
}
|
|
|
|
/**
|
|
* Register REST API endpoints
|
|
*/
|
|
public function register_endpoints() {
|
|
add_action('rest_api_init', function() {
|
|
// Login endpoint
|
|
register_rest_route('twilio-mobile/v1', '/auth/login', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_login'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// Refresh token endpoint
|
|
register_rest_route('twilio-mobile/v1', '/auth/refresh', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_refresh'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// Logout endpoint
|
|
register_rest_route('twilio-mobile/v1', '/auth/logout', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_logout'),
|
|
'permission_callback' => array($this, 'verify_token')
|
|
));
|
|
|
|
// Validate token endpoint (for debugging)
|
|
register_rest_route('twilio-mobile/v1', '/auth/validate', array(
|
|
'methods' => 'GET',
|
|
'callback' => array($this, 'handle_validate'),
|
|
'permission_callback' => array($this, 'verify_token')
|
|
));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle login request
|
|
*/
|
|
public function handle_login($request) {
|
|
$username = $request->get_param('username');
|
|
$password = $request->get_param('password');
|
|
$fcm_token = $request->get_param('fcm_token'); // Optional
|
|
$device_info = $request->get_param('device_info'); // Optional
|
|
|
|
if (empty($username) || empty($password)) {
|
|
return new WP_Error('missing_credentials', 'Username and password are required', array('status' => 400));
|
|
}
|
|
|
|
// Authenticate user
|
|
$user = wp_authenticate($username, $password);
|
|
|
|
if (is_wp_error($user)) {
|
|
return new WP_Error('invalid_credentials', 'Invalid username or password', array('status' => 401));
|
|
}
|
|
|
|
// Check if user has phone agent capabilities
|
|
if (!user_can($user->ID, 'twp_access_browser_phone') && !user_can($user->ID, 'manage_options')) {
|
|
return new WP_Error('insufficient_permissions', 'User does not have phone agent access', array('status' => 403));
|
|
}
|
|
|
|
// Generate tokens
|
|
$access_token = $this->generate_token($user->ID, 'access');
|
|
$refresh_token = $this->generate_token($user->ID, 'refresh');
|
|
|
|
// Store session in database
|
|
$this->store_session($user->ID, $refresh_token, $fcm_token, $device_info);
|
|
|
|
// Get user data
|
|
$user_data = $this->get_user_data($user->ID);
|
|
|
|
return new WP_REST_Response(array(
|
|
'success' => true,
|
|
'access_token' => $access_token,
|
|
'refresh_token' => $refresh_token,
|
|
'expires_in' => $this->token_expiry,
|
|
'user' => $user_data
|
|
), 200);
|
|
}
|
|
|
|
/**
|
|
* Handle token refresh request
|
|
*/
|
|
public function handle_refresh($request) {
|
|
$refresh_token = $request->get_param('refresh_token');
|
|
|
|
if (empty($refresh_token)) {
|
|
return new WP_Error('missing_token', 'Refresh token is required', array('status' => 400));
|
|
}
|
|
|
|
// Verify refresh token
|
|
$payload = $this->decode_token($refresh_token);
|
|
|
|
if (!$payload || $payload->type !== 'refresh') {
|
|
return new WP_Error('invalid_token', 'Invalid refresh token', array('status' => 401));
|
|
}
|
|
|
|
// Check if session exists and is valid
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
|
|
|
$session = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $table WHERE user_id = %d AND refresh_token = %s AND is_active = 1 AND expires_at > NOW()",
|
|
$payload->user_id,
|
|
$refresh_token
|
|
));
|
|
|
|
if (!$session) {
|
|
return new WP_Error('invalid_session', 'Session expired or invalid', array('status' => 401));
|
|
}
|
|
|
|
// Generate new access token
|
|
$access_token = $this->generate_token($payload->user_id, 'access');
|
|
|
|
// Update last_used timestamp
|
|
$wpdb->update(
|
|
$table,
|
|
array('last_used' => current_time('mysql')),
|
|
array('id' => $session->id),
|
|
array('%s'),
|
|
array('%d')
|
|
);
|
|
|
|
return new WP_REST_Response(array(
|
|
'success' => true,
|
|
'access_token' => $access_token,
|
|
'expires_in' => $this->token_expiry
|
|
), 200);
|
|
}
|
|
|
|
/**
|
|
* Handle logout request
|
|
*/
|
|
public function handle_logout($request) {
|
|
$user_id = $this->get_current_user_id();
|
|
|
|
if (!$user_id) {
|
|
return new WP_Error('unauthorized', 'Invalid token', array('status' => 401));
|
|
}
|
|
|
|
// Get refresh token from request
|
|
$refresh_token = $request->get_param('refresh_token');
|
|
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
|
|
|
if ($refresh_token) {
|
|
// Invalidate specific session
|
|
$wpdb->update(
|
|
$table,
|
|
array('is_active' => 0),
|
|
array('user_id' => $user_id, 'refresh_token' => $refresh_token),
|
|
array('%d'),
|
|
array('%d', '%s')
|
|
);
|
|
} else {
|
|
// Invalidate all sessions for this user
|
|
$wpdb->update(
|
|
$table,
|
|
array('is_active' => 0),
|
|
array('user_id' => $user_id),
|
|
array('%d'),
|
|
array('%d')
|
|
);
|
|
}
|
|
|
|
return new WP_REST_Response(array(
|
|
'success' => true,
|
|
'message' => 'Logged out successfully'
|
|
), 200);
|
|
}
|
|
|
|
/**
|
|
* Handle token validation request
|
|
*/
|
|
public function handle_validate($request) {
|
|
$user_id = $this->get_current_user_id();
|
|
|
|
if (!$user_id) {
|
|
return new WP_Error('unauthorized', 'Invalid token', array('status' => 401));
|
|
}
|
|
|
|
$user_data = $this->get_user_data($user_id);
|
|
|
|
return new WP_REST_Response(array(
|
|
'success' => true,
|
|
'valid' => true,
|
|
'user' => $user_data
|
|
), 200);
|
|
}
|
|
|
|
/**
|
|
* Generate JWT token
|
|
*/
|
|
private function generate_token($user_id, $type = 'access') {
|
|
$issued_at = time();
|
|
$expiry = $type === 'refresh' ? $this->refresh_expiry : $this->token_expiry;
|
|
|
|
$payload = array(
|
|
'iat' => $issued_at,
|
|
'exp' => $issued_at + $expiry,
|
|
'user_id' => $user_id,
|
|
'type' => $type
|
|
);
|
|
|
|
return $this->encode_token($payload);
|
|
}
|
|
|
|
/**
|
|
* Simple JWT encoding (header.payload.signature)
|
|
*/
|
|
private function encode_token($payload) {
|
|
$header = array('typ' => 'JWT', 'alg' => 'HS256');
|
|
|
|
$segments = array();
|
|
$segments[] = $this->base64url_encode(json_encode($header));
|
|
$segments[] = $this->base64url_encode(json_encode($payload));
|
|
|
|
$signing_input = implode('.', $segments);
|
|
$signature = hash_hmac('sha256', $signing_input, $this->secret_key, true);
|
|
$segments[] = $this->base64url_encode($signature);
|
|
|
|
return implode('.', $segments);
|
|
}
|
|
|
|
/**
|
|
* Simple JWT decoding
|
|
*/
|
|
private function decode_token($token) {
|
|
$segments = explode('.', $token);
|
|
|
|
if (count($segments) !== 3) {
|
|
return false;
|
|
}
|
|
|
|
list($header_b64, $payload_b64, $signature_b64) = $segments;
|
|
|
|
// Verify signature
|
|
$signing_input = $header_b64 . '.' . $payload_b64;
|
|
$signature = $this->base64url_decode($signature_b64);
|
|
$expected_signature = hash_hmac('sha256', $signing_input, $this->secret_key, true);
|
|
|
|
if (!hash_equals($signature, $expected_signature)) {
|
|
return false;
|
|
}
|
|
|
|
// Decode payload
|
|
$payload = json_decode($this->base64url_decode($payload_b64));
|
|
|
|
if (!$payload) {
|
|
return false;
|
|
}
|
|
|
|
// Check expiration
|
|
if (isset($payload->exp) && $payload->exp < time()) {
|
|
return false;
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
/**
|
|
* Base64 URL encode
|
|
*/
|
|
private function base64url_encode($data) {
|
|
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
|
}
|
|
|
|
/**
|
|
* Base64 URL decode
|
|
*/
|
|
private function base64url_decode($data) {
|
|
return base64_decode(strtr($data, '-_', '+/'));
|
|
}
|
|
|
|
/**
|
|
* Verify token (permission callback)
|
|
*/
|
|
public function verify_token($request) {
|
|
$auth_header = $request->get_header('Authorization');
|
|
|
|
if (empty($auth_header)) {
|
|
return false;
|
|
}
|
|
|
|
// Extract token from "Bearer <token>"
|
|
if (preg_match('/Bearer\s+(.*)$/i', $auth_header, $matches)) {
|
|
$token = $matches[1];
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
$payload = $this->decode_token($token);
|
|
|
|
if (!$payload || $payload->type !== 'access') {
|
|
return false;
|
|
}
|
|
|
|
// Store user ID for later use
|
|
$this->current_user_id = $payload->user_id;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get current user ID from token
|
|
*/
|
|
public function get_current_user_id() {
|
|
return $this->current_user_id;
|
|
}
|
|
|
|
/**
|
|
* Store session in database
|
|
*/
|
|
private function store_session($user_id, $refresh_token, $fcm_token = null, $device_info = null) {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
|
|
|
$wpdb->insert(
|
|
$table,
|
|
array(
|
|
'user_id' => $user_id,
|
|
'refresh_token' => $refresh_token,
|
|
'fcm_token' => $fcm_token,
|
|
'device_info' => $device_info,
|
|
'created_at' => current_time('mysql'),
|
|
'expires_at' => date('Y-m-d H:i:s', time() + $this->refresh_expiry),
|
|
'last_used' => current_time('mysql'),
|
|
'is_active' => 1
|
|
),
|
|
array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get user data for response
|
|
*/
|
|
private function get_user_data($user_id) {
|
|
$user = get_userdata($user_id);
|
|
|
|
if (!$user) {
|
|
return null;
|
|
}
|
|
|
|
// Get agent phone number
|
|
$agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
|
|
|
|
// Get agent status
|
|
global $wpdb;
|
|
$status_table = $wpdb->prefix . 'twp_agent_status';
|
|
$status = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT status, is_logged_in, current_call_sid FROM $status_table WHERE user_id = %d",
|
|
$user_id
|
|
));
|
|
|
|
// Get user extension
|
|
$ext_table = $wpdb->prefix . 'twp_user_extensions';
|
|
$extension = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT extension, direct_dial_number FROM $ext_table WHERE user_id = %d",
|
|
$user_id
|
|
));
|
|
|
|
return array(
|
|
'id' => $user->ID,
|
|
'username' => $user->user_login,
|
|
'display_name' => $user->display_name,
|
|
'email' => $user->user_email,
|
|
'phone_number' => $agent_number,
|
|
'extension' => $extension ? $extension->extension : null,
|
|
'direct_dial' => $extension ? $extension->direct_dial_number : null,
|
|
'status' => $status ? $status->status : 'offline',
|
|
'is_logged_in' => $status ? (bool)$status->is_logged_in : false,
|
|
'current_call_sid' => $status ? $status->current_call_sid : null,
|
|
'capabilities' => array(
|
|
'can_access_browser_phone' => user_can($user_id, 'twp_access_browser_phone'),
|
|
'can_access_voicemails' => user_can($user_id, 'twp_access_voicemails'),
|
|
'can_access_call_log' => user_can($user_id, 'twp_access_call_log'),
|
|
'can_access_agent_queue' => user_can($user_id, 'twp_access_agent_queue'),
|
|
'can_access_sms_inbox' => user_can($user_id, 'twp_access_sms_inbox'),
|
|
'is_admin' => user_can($user_id, 'manage_options')
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Update FCM token for existing session
|
|
*/
|
|
public function update_fcm_token($user_id, $refresh_token, $fcm_token) {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
|
|
|
$wpdb->update(
|
|
$table,
|
|
array('fcm_token' => $fcm_token),
|
|
array('user_id' => $user_id, 'refresh_token' => $refresh_token, 'is_active' => 1),
|
|
array('%s'),
|
|
array('%d', '%s', '%d')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get all active FCM tokens for a user
|
|
*/
|
|
public function get_user_fcm_tokens($user_id) {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
|
|
|
return $wpdb->get_col($wpdb->prepare(
|
|
"SELECT fcm_token FROM $table WHERE user_id = %d AND is_active = 1 AND fcm_token IS NOT NULL AND expires_at > NOW()",
|
|
$user_id
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Clean up expired sessions
|
|
*/
|
|
public static function cleanup_expired_sessions() {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
|
|
|
$wpdb->query("UPDATE $table SET is_active = 0 WHERE expires_at < NOW() AND is_active = 1");
|
|
}
|
|
}
|