From a50a4f274f0cb32e457fd34532f42a5529b94c15 Mon Sep 17 00:00:00 2001 From: cs2000 Date: Wed, 4 Feb 2026 13:23:53 +0000 Subject: [PATCH] Changes for M365 oAuth - Added handler to start Microsoft OAuth Authorization Code flow (oauth_connect_microsoft_mail) with state generation/validation prep. - Added handler to test OAuth token refresh from admin UI and persist refreshed tokens/expiry. - Updated IMAP test handler to support OAuth token refresh + XOAUTH2 authentication (in addition to legacy LOGIN). --- admin/post/settings_mail.php | 865 +++++++++++++++++++++++------------ 1 file changed, 578 insertions(+), 287 deletions(-) diff --git a/admin/post/settings_mail.php b/admin/post/settings_mail.php index 34d9dc0e..7e5f1d04 100644 --- a/admin/post/settings_mail.php +++ b/admin/post/settings_mail.php @@ -1,287 +1,578 @@ - $email_from, - 'from_name' => $email_from_name, - 'recipient' => $email_to, - 'recipient_name' => 'Chap', - 'subject' => $subject, - 'body' => $body - ] - ]; - - $mail = addToMailQueue($data); - - if ($mail === true) { - flash_alert("Test email queued! Check Admin > Mail queue"); - } else { - flash_alert("Failed to add test mail to queue", 'error'); - } - - redirect(); - -} - -if (isset($_POST['test_email_imap'])) { - - validateCSRFToken($_POST['csrf_token']); - - $host = $config_imap_host; - $port = (int) $config_imap_port; - $encryption = strtolower(trim($config_imap_encryption)); // e.g. "ssl", "tls", "none" - $username = $config_imap_username; - $password = $config_imap_password; - - // Build remote socket (implicit SSL vs plain TCP) - $transport = 'tcp'; - if ($encryption === 'ssl') { - $transport = 'ssl'; - } - - $remote_socket = $transport . '://' . $host . ':' . $port; - - // Stream context (you can tighten these if you want strict validation) - $contextOptions = []; - if (in_array($encryption, ['ssl', 'tls'], true)) { - $contextOptions['ssl'] = [ - 'verify_peer' => false, - 'verify_peer_name' => false, - 'allow_self_signed' => true, - ]; - } - - $context = stream_context_create($contextOptions); - - try { - $errno = 0; - $errstr = ''; - - // 10-second timeout, adjust as needed - $fp = @stream_socket_client( - $remote_socket, - $errno, - $errstr, - 10, - STREAM_CLIENT_CONNECT, - $context - ); - - if (!$fp) { - throw new Exception("Could not connect to IMAP server: [$errno] $errstr"); - } - - stream_set_timeout($fp, 10); - - // Read server greeting (IMAP servers send something like: * OK Dovecot ready) - $greeting = fgets($fp, 1024); - if ($greeting === false || strpos($greeting, '* OK') !== 0) { - fclose($fp); - throw new Exception("Invalid IMAP greeting: " . trim((string) $greeting)); - } - - // If you really want STARTTLS for "tls" (port 143), you can do it here - if ($encryption === 'tls' && stripos($greeting, 'STARTTLS') !== false) { - // Request STARTTLS - fwrite($fp, "A0001 STARTTLS\r\n"); - $line = fgets($fp, 1024); - if ($line === false || stripos($line, 'A0001 OK') !== 0) { - fclose($fp); - throw new Exception("STARTTLS failed: " . trim((string) $line)); - } - - // Enable crypto on the stream - if (!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { - fclose($fp); - throw new Exception("Unable to enable TLS encryption on IMAP connection."); - } - } - - // --- Do LOGIN command --- - $tag = 'A0002'; - - // Simple quoting; this may fail with some special chars in username/password. - $loginCmd = sprintf( - "%s LOGIN \"%s\" \"%s\"\r\n", - $tag, - addcslashes($username, "\\\""), - addcslashes($password, "\\\"") - ); - - fwrite($fp, $loginCmd); - - $success = false; - $errorLine = ''; - - while (!feof($fp)) { - $line = fgets($fp, 2048); - if ($line === false) { - break; - } - - // Look for tagged response for our LOGIN - if (strpos($line, $tag . ' ') === 0) { - if (stripos($line, $tag . ' OK') === 0) { - $success = true; - } else { - $errorLine = trim($line); - } - break; - } - } - - // Always logout / close - fwrite($fp, "A0003 LOGOUT\r\n"); - fclose($fp); - - if ($success) { - flash_alert("Connected successfully"); - } else { - if (!$errorLine) { - $errorLine = 'Unknown IMAP authentication error'; - } - throw new Exception($errorLine); - } - - } catch (Exception $e) { - flash_alert("IMAP connection failed: " . htmlspecialchars($e->getMessage()), 'error'); - } - - redirect(); -} + $config_mail_oauth_client_id, + 'response_type' => 'code', + 'redirect_uri' => $redirect_uri, + 'response_mode' => 'query', + 'scope' => $scope, + 'state' => $state, + 'prompt' => 'consent', + ], '', '&', PHP_QUERY_RFC3986); + + logAction("Settings", "Edit", "$session_name started Microsoft OAuth connect flow for mail settings"); + + redirect($authorize_url); +} + +if (isset($_POST['edit_mail_smtp_settings'])) { + + validateCSRFToken($_POST['csrf_token']); + + $config_smtp_provider = sanitizeInput($_POST['config_smtp_provider']); + $config_smtp_host = sanitizeInput($_POST['config_smtp_host']); + $config_smtp_port = intval($_POST['config_smtp_port'] ?? 0); + $config_smtp_encryption = sanitizeInput($_POST['config_smtp_encryption']); + $config_smtp_username = sanitizeInput($_POST['config_smtp_username']); + $config_smtp_password = sanitizeInput($_POST['config_smtp_password']); + + // Shared OAuth fields + $config_mail_oauth_client_id = sanitizeInput($_POST['config_mail_oauth_client_id']); + $config_mail_oauth_client_secret = sanitizeInput($_POST['config_mail_oauth_client_secret']); + $config_mail_oauth_tenant_id = sanitizeInput($_POST['config_mail_oauth_tenant_id']); + $config_mail_oauth_refresh_token = sanitizeInput($_POST['config_mail_oauth_refresh_token']); + $config_mail_oauth_access_token = sanitizeInput($_POST['config_mail_oauth_access_token']); + + mysqli_query($mysqli, " + UPDATE settings SET + config_smtp_provider = '$config_smtp_provider', + config_smtp_host = '$config_smtp_host', + config_smtp_port = $config_smtp_port, + config_smtp_encryption = '$config_smtp_encryption', + config_smtp_username = '$config_smtp_username', + config_smtp_password = '$config_smtp_password', + config_mail_oauth_client_id = '$config_mail_oauth_client_id', + config_mail_oauth_client_secret = '$config_mail_oauth_client_secret', + config_mail_oauth_tenant_id = '$config_mail_oauth_tenant_id', + config_mail_oauth_refresh_token = '$config_mail_oauth_refresh_token', + config_mail_oauth_access_token = '$config_mail_oauth_access_token' + WHERE company_id = 1 + "); + + logAction("Settings", "Edit", "$session_name edited SMTP settings"); + + flash_alert("SMTP Mail Settings updated"); + + redirect(); + +} + +if (isset($_POST['edit_mail_imap_settings'])) { + + validateCSRFToken($_POST['csrf_token']); + + $config_imap_provider = sanitizeInput($_POST['config_imap_provider']); + $config_imap_host = sanitizeInput($_POST['config_imap_host']); + $config_imap_port = intval($_POST['config_imap_port'] ?? 0); + $config_imap_encryption = sanitizeInput($_POST['config_imap_encryption']); + $config_imap_username = sanitizeInput($_POST['config_imap_username']); + $config_imap_password = sanitizeInput($_POST['config_imap_password']); + + // Shared OAuth fields + $config_mail_oauth_client_id = sanitizeInput($_POST['config_mail_oauth_client_id']); + $config_mail_oauth_client_secret = sanitizeInput($_POST['config_mail_oauth_client_secret']); + $config_mail_oauth_tenant_id = sanitizeInput($_POST['config_mail_oauth_tenant_id']); + $config_mail_oauth_refresh_token = sanitizeInput($_POST['config_mail_oauth_refresh_token']); + $config_mail_oauth_access_token = sanitizeInput($_POST['config_mail_oauth_access_token']); + + mysqli_query($mysqli, " + UPDATE settings SET + config_imap_provider = '$config_imap_provider', + config_imap_host = '$config_imap_host', + config_imap_port = $config_imap_port, + config_imap_encryption = '$config_imap_encryption', + config_imap_username = '$config_imap_username', + config_imap_password = '$config_imap_password', + config_mail_oauth_client_id = '$config_mail_oauth_client_id', + config_mail_oauth_client_secret = '$config_mail_oauth_client_secret', + config_mail_oauth_tenant_id = '$config_mail_oauth_tenant_id', + config_mail_oauth_refresh_token = '$config_mail_oauth_refresh_token', + config_mail_oauth_access_token = '$config_mail_oauth_access_token' + WHERE company_id = 1 + "); + + logAction("Settings", "Edit", "$session_name edited IMAP settings"); + + flash_alert("IMAP Mail Settings updated"); + + redirect(); + +} + +if (isset($_POST['edit_mail_from_settings'])) { + + validateCSRFToken($_POST['csrf_token']); + + $config_mail_from_email = sanitizeInput(filter_var($_POST['config_mail_from_email'], FILTER_VALIDATE_EMAIL)); + $config_mail_from_name = sanitizeInput(preg_replace('/[^a-zA-Z0-9\s]/', '', $_POST['config_mail_from_name'])); + + $config_invoice_from_email = sanitizeInput(filter_var($_POST['config_invoice_from_email'], FILTER_VALIDATE_EMAIL)); + $config_invoice_from_name = sanitizeInput(preg_replace('/[^a-zA-Z0-9\s]/', '', $_POST['config_invoice_from_name'])); + + $config_quote_from_email = sanitizeInput(filter_var($_POST['config_quote_from_email'], FILTER_VALIDATE_EMAIL)); + $config_quote_from_name = sanitizeInput(preg_replace('/[^a-zA-Z0-9\s]/', '', $_POST['config_quote_from_name'])); + + $config_ticket_from_email = sanitizeInput(filter_var($_POST['config_ticket_from_email'], FILTER_VALIDATE_EMAIL)); + $config_ticket_from_name = sanitizeInput(preg_replace('/[^a-zA-Z0-9\s]/', '', $_POST['config_ticket_from_name'])); + + mysqli_query($mysqli,"UPDATE settings SET config_mail_from_email = '$config_mail_from_email', config_mail_from_name = '$config_mail_from_name', config_invoice_from_email = '$config_invoice_from_email', config_invoice_from_name = '$config_invoice_from_name', config_quote_from_email = '$config_quote_from_email', config_quote_from_name = '$config_quote_from_name', config_ticket_from_email = '$config_ticket_from_email', config_ticket_from_name = '$config_ticket_from_name' WHERE company_id = 1"); + + logAction("Settings", "Edit", "$session_name edited mail from settings"); + + flash_alert("Mail From Settings updated"); + + redirect(); + +} + +if (isset($_POST['test_email_smtp'])) { + + validateCSRFToken($_POST['csrf_token']); + + $test_email = intval($_POST['test_email']); + + if($test_email == 1) { + $email_from = sanitizeInput($config_mail_from_email); + $email_from_name = sanitizeInput($config_mail_from_name); + } elseif ($test_email == 2) { + $email_from = sanitizeInput($config_invoice_from_email); + $email_from_name = sanitizeInput($config_invoice_from_name); + } elseif ($test_email == 3) { + $email_from = sanitizeInput($config_quote_from_email); + $email_from_name = sanitizeInput($config_quote_from_name); + } else { + $email_from = sanitizeInput($config_ticket_from_email); + $email_from_name = sanitizeInput($config_ticket_from_name); + } + + $email_to = sanitizeInput($_POST['email_to']); + $subject = "Test email from ITFlow"; + $body = "This is a test email from ITFlow. If you are reading this, it worked!"; + + $data = [ + [ + 'from' => $email_from, + 'from_name' => $email_from_name, + 'recipient' => $email_to, + 'recipient_name' => 'Chap', + 'subject' => $subject, + 'body' => $body + ] + ]; + + $mail = addToMailQueue($data); + + if ($mail === true) { + flash_alert("Test email queued! Check Admin > Mail queue"); + } else { + flash_alert("Failed to add test mail to queue", 'error'); + } + + redirect(); + +} + +if (isset($_POST['test_email_imap'])) { + + validateCSRFToken($_POST['csrf_token']); + + $provider = sanitizeInput($config_imap_provider ?? ''); + + $host = $config_imap_host; + $port = (int) $config_imap_port; + $encryption = strtolower(trim($config_imap_encryption)); // e.g. "ssl", "tls", "none" + $username = $config_imap_username; + $password = $config_imap_password; + + // Shared OAuth fields + $config_mail_oauth_client_id = $config_mail_oauth_client_id ?? ''; + $config_mail_oauth_client_secret = $config_mail_oauth_client_secret ?? ''; + $config_mail_oauth_tenant_id = $config_mail_oauth_tenant_id ?? ''; + $config_mail_oauth_refresh_token = $config_mail_oauth_refresh_token ?? ''; + $config_mail_oauth_access_token = $config_mail_oauth_access_token ?? ''; + $config_mail_oauth_access_token_expires_at = $config_mail_oauth_access_token_expires_at ?? ''; + + $is_oauth = ($provider === 'google_oauth' || $provider === 'microsoft_oauth'); + + if ($provider === 'google_oauth') { + if (empty($host)) { + $host = 'imap.gmail.com'; + } + if (empty($port)) { + $port = 993; + } + if (empty($encryption)) { + $encryption = 'ssl'; + } + } elseif ($provider === 'microsoft_oauth') { + if (empty($host)) { + $host = 'outlook.office365.com'; + } + if (empty($port)) { + $port = 993; + } + if (empty($encryption)) { + $encryption = 'ssl'; + } + } + + if (empty($host) || empty($port) || empty($username)) { + flash_alert("IMAP connection failed: Missing host, port, or username.", 'error'); + redirect(); + } + + $token_is_expired = function (?string $expires_at): bool { + if (empty($expires_at)) { + return true; + } + + $ts = strtotime($expires_at); + + if ($ts === false) { + return true; + } + + return ($ts - 60) <= time(); + }; + + $http_form_post = function (string $url, array $fields): array { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields, '', '&')); + curl_setopt($ch, CURLOPT_TIMEOUT, 20); + + $raw = curl_exec($ch); + $err = curl_error($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + curl_close($ch); + + return [ + 'ok' => ($raw !== false && $code >= 200 && $code < 300), + 'body' => $raw, + 'code' => $code, + 'err' => $err, + ]; + }; + + if ($is_oauth) { + if (!empty($config_mail_oauth_access_token) && !$token_is_expired($config_mail_oauth_access_token_expires_at)) { + $password = $config_mail_oauth_access_token; + } else { + if (empty($config_mail_oauth_client_id) || empty($config_mail_oauth_client_secret) || empty($config_mail_oauth_refresh_token)) { + flash_alert("IMAP OAuth failed: Missing OAuth client credentials or refresh token.", 'error'); + redirect(); + } + + if ($provider === 'google_oauth') { + $response = $http_form_post('https://oauth2.googleapis.com/token', [ + 'client_id' => $config_mail_oauth_client_id, + 'client_secret' => $config_mail_oauth_client_secret, + 'refresh_token' => $config_mail_oauth_refresh_token, + 'grant_type' => 'refresh_token', + ]); + } else { + if (empty($config_mail_oauth_tenant_id)) { + flash_alert("IMAP OAuth failed: Microsoft tenant ID is required.", 'error'); + redirect(); + } + + $token_url = "https://login.microsoftonline.com/" . rawurlencode($config_mail_oauth_tenant_id) . "/oauth2/v2.0/token"; + $response = $http_form_post($token_url, [ + 'client_id' => $config_mail_oauth_client_id, + 'client_secret' => $config_mail_oauth_client_secret, + 'refresh_token' => $config_mail_oauth_refresh_token, + 'grant_type' => 'refresh_token', + ]); + } + + if (!$response['ok']) { + flash_alert("IMAP OAuth failed: Could not refresh access token.", 'error'); + redirect(); + } + + $json = json_decode($response['body'], true); + if (!is_array($json) || empty($json['access_token'])) { + flash_alert("IMAP OAuth failed: Token response did not include an access token.", 'error'); + redirect(); + } + + $password = $json['access_token']; + $expires_at = date('Y-m-d H:i:s', time() + (int)($json['expires_in'] ?? 3600)); + $refresh_token_to_save = $json['refresh_token'] ?? null; + + $token_esc = mysqli_real_escape_string($mysqli, $password); + $expires_at_esc = mysqli_real_escape_string($mysqli, $expires_at); + + $refresh_sql = ''; + if (!empty($refresh_token_to_save)) { + $refresh_token_esc = mysqli_real_escape_string($mysqli, $refresh_token_to_save); + $refresh_sql = ", config_mail_oauth_refresh_token = '{$refresh_token_esc}'"; + } + + mysqli_query($mysqli, "UPDATE settings SET config_mail_oauth_access_token = '{$token_esc}', config_mail_oauth_access_token_expires_at = '{$expires_at_esc}'{$refresh_sql} WHERE company_id = 1"); + } + } + + // Build remote socket (implicit SSL vs plain TCP) + $transport = 'tcp'; + if ($encryption === 'ssl') { + $transport = 'ssl'; + } + + $remote_socket = $transport . '://' . $host . ':' . $port; + + // Stream context (you can tighten these if you want strict validation) + $context_options = []; + if (in_array($encryption, ['ssl', 'tls'], true)) { + $context_options['ssl'] = [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, + ]; + } + + $context = stream_context_create($context_options); + + try { + $errno = 0; + $errstr = ''; + + // 10-second timeout, adjust as needed + $fp = @stream_socket_client( + $remote_socket, + $errno, + $errstr, + 10, + STREAM_CLIENT_CONNECT, + $context + ); + + if (!$fp) { + throw new Exception("Could not connect to IMAP server: [$errno] $errstr"); + } + + stream_set_timeout($fp, 10); + + // Read server greeting (IMAP servers send something like: * OK Dovecot ready) + $greeting = fgets($fp, 1024); + if ($greeting === false || strpos($greeting, '* OK') !== 0) { + fclose($fp); + throw new Exception("Invalid IMAP greeting: " . trim((string) $greeting)); + } + // If you really want STARTTLS for "tls" (port 143), you can do it here + if ($encryption === 'tls' && stripos($greeting, 'STARTTLS') !== false) { + fwrite($fp, "A0001 STARTTLS\r\n"); + $line = fgets($fp, 1024); + if ($line === false || stripos($line, 'A0001 OK') !== 0) { + fclose($fp); + throw new Exception("STARTTLS failed: " . trim((string) $line)); + } + + if (!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { + fclose($fp); + throw new Exception("Unable to enable TLS encryption on IMAP connection."); + } + } + + $tag = 'A0002'; + + if ($is_oauth) { + $oauth_b64 = base64_encode("user={$username}\x01auth=Bearer {$password}\x01\x01"); + $auth_cmd = sprintf("%s AUTHENTICATE XOAUTH2 %s\r\n", $tag, $oauth_b64); + fwrite($fp, $auth_cmd); + } else { + $login_cmd = sprintf( + "%s LOGIN \"%s\" \"%s\"\r\n", + $tag, + addcslashes($username, "\\\""), + addcslashes($password, "\\\"") + ); + + fwrite($fp, $login_cmd); + } + + $success = false; + $error_line = ''; + + while (!feof($fp)) { + $line = fgets($fp, 2048); + if ($line === false) { + break; + } + + if (strpos($line, $tag . ' ') === 0) { + if (stripos($line, $tag . ' OK') === 0) { + $success = true; + } else { + $error_line = trim($line); + } + break; + } + } + + // Always logout / close + fwrite($fp, "A0003 LOGOUT\r\n"); + fclose($fp); + + if ($success) { + if ($is_oauth) { + flash_alert("Connected successfully using OAuth"); + } else { + flash_alert("Connected successfully"); + } + } else { + if (!$error_line) { + $error_line = 'Unknown IMAP authentication error'; + } + throw new Exception($error_line); + } + + } catch (Exception $e) { + flash_alert("IMAP connection failed: " . htmlspecialchars($e->getMessage()), 'error'); + } + + redirect(); +} + + +if (isset($_POST['test_oauth_token_refresh'])) { + + validateCSRFToken($_POST['csrf_token']); + + $provider = sanitizeInput($_POST['oauth_provider'] ?? ''); + + if ($provider !== 'google_oauth' && $provider !== 'microsoft_oauth') { + flash_alert("OAuth token test failed: unsupported provider.", 'error'); + redirect(); + } + + $oauth_client_id = sanitizeInput($config_mail_oauth_client_id ?? ''); + $oauth_client_secret = sanitizeInput($config_mail_oauth_client_secret ?? ''); + $oauth_tenant_id = sanitizeInput($config_mail_oauth_tenant_id ?? ''); + $oauth_refresh_token = sanitizeInput($config_mail_oauth_refresh_token ?? ''); + + if (empty($oauth_client_id) || empty($oauth_client_secret) || empty($oauth_refresh_token)) { + flash_alert("OAuth token test failed: missing client ID, client secret, or refresh token.", 'error'); + redirect(); + } + + if ($provider === 'microsoft_oauth' && empty($oauth_tenant_id)) { + flash_alert("OAuth token test failed: Microsoft tenant ID is required.", 'error'); + redirect(); + } + + $token_url = 'https://oauth2.googleapis.com/token'; + if ($provider === 'microsoft_oauth') { + $token_url = "https://login.microsoftonline.com/" . rawurlencode($oauth_tenant_id) . "/oauth2/v2.0/token"; + } + + $post_fields = http_build_query([ + 'client_id' => $oauth_client_id, + 'client_secret' => $oauth_client_secret, + 'refresh_token' => $oauth_refresh_token, + 'grant_type' => 'refresh_token', + ]); + + $ch = curl_init($token_url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields); + curl_setopt($ch, CURLOPT_TIMEOUT, 20); + + $raw_body = curl_exec($ch); + $curl_err = curl_error($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($raw_body === false || $http_code < 200 || $http_code >= 300) { + $err_msg = !empty($curl_err) ? $curl_err : "HTTP $http_code"; + flash_alert("OAuth token test failed: $err_msg", 'error'); + redirect(); + } + + $json = json_decode($raw_body, true); + + if (!is_array($json) || empty($json['access_token'])) { + flash_alert("OAuth token test failed: access token missing in provider response.", 'error'); + redirect(); + } + + $new_access_token = sanitizeInput($json['access_token']); + $new_expires_at = date('Y-m-d H:i:s', time() + (int)($json['expires_in'] ?? 3600)); + $new_refresh_token = !empty($json['refresh_token']) ? sanitizeInput($json['refresh_token']) : ''; + + $new_access_token_esc = mysqli_real_escape_string($mysqli, $new_access_token); + $new_expires_at_esc = mysqli_real_escape_string($mysqli, $new_expires_at); + + $refresh_sql = ''; + if (!empty($new_refresh_token)) { + $new_refresh_token_esc = mysqli_real_escape_string($mysqli, $new_refresh_token); + $refresh_sql = ", config_mail_oauth_refresh_token = '$new_refresh_token_esc'"; + } + + mysqli_query($mysqli, "UPDATE settings SET config_mail_oauth_access_token = '$new_access_token_esc', config_mail_oauth_access_token_expires_at = '$new_expires_at_esc'$refresh_sql WHERE company_id = 1"); + + $provider_label = $provider === 'microsoft_oauth' ? 'Microsoft 365' : 'Google Workspace'; + logAction("Settings", "Edit", "$session_name tested OAuth token refresh for $provider_label mail settings"); + + flash_alert("OAuth token refresh successful for $provider_label. Access token expires at $new_expires_at."); + redirect(); +}